loading
Generated 2025-10-17T15:44:01+08:00

All Files ( 3.72% covered at 0.11 hits/line )

114 files in total.
20542 relevant lines, 765 lines covered and 19777 lines missed. ( 3.72% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/api/admin/admin_controller.rb 0.00 % 188 157 0 157 0.00
app/controllers/api/application_controller.rb 0.00 % 179 137 0 137 0.00
app/controllers/api/auth_controller.rb 0.00 % 108 82 0 82 0.00
app/controllers/api/check_in_comments_controller.rb 0.00 % 42 34 0 34 0.00
app/controllers/api/check_ins_controller.rb 0.00 % 120 100 0 100 0.00
app/controllers/api/comments_controller.rb 0.00 % 26 20 0 20 0.00
app/controllers/api/daily_leadings_controller.rb 0.00 % 124 99 0 99 0.00
app/controllers/api/events_controller.rb 0.00 % 233 171 0 171 0.00
app/controllers/api/flowers_controller.rb 0.00 % 121 105 0 105 0.00
app/controllers/api/likes_controller.rb 0.00 % 56 46 0 46 0.00
app/controllers/api/optimized_posts_controller.rb 0.00 % 181 130 0 130 0.00
app/controllers/api/posts_controller.rb 0.00 % 199 142 0 142 0.00
app/controllers/api/uploads_controller.rb 0.00 % 55 36 0 36 0.00
app/controllers/api/v1/analytics_controller.rb 0.00 % 464 378 0 378 0.00
app/controllers/api/v1/approval_workflow_controller.rb 0.00 % 593 498 0 498 0.00
app/controllers/api/v1/base_controller.rb 0.00 % 302 231 0 231 0.00
app/controllers/api/v1/check_ins_controller.rb 0.00 % 727 590 0 590 0.00
app/controllers/api/v1/content_export_controller.rb 0.00 % 410 309 0 309 0.00
app/controllers/api/v1/content_reports_controller.rb 0.00 % 478 398 0 398 0.00
app/controllers/api/v1/content_search_controller.rb 0.00 % 359 269 0 269 0.00
app/controllers/api/v1/daily_leadings_controller.rb 0.00 % 278 224 0 224 0.00
app/controllers/api/v1/event_enrollments_controller.rb 0.00 % 343 264 0 264 0.00
app/controllers/api/v1/flower_comments_controller.rb 0.00 % 79 53 0 53 0.00
app/controllers/api/v1/flower_incentives_controller.rb 0.00 % 287 237 0 237 0.00
app/controllers/api/v1/flower_leaderboards_controller.rb 0.00 % 342 288 0 288 0.00
app/controllers/api/v1/leader_assignments_controller.rb 0.00 % 320 276 0 276 0.00
app/controllers/api/v1/notifications_controller.rb 0.00 % 203 145 0 145 0.00
app/controllers/api/v1/performance_posts_controller.rb 0.00 % 376 286 0 286 0.00
app/controllers/api/v1/reading_events_controller.rb 0.00 % 419 354 0 354 0.00
app/controllers/api/v1/reading_schedules_controller.rb 0.00 % 286 236 0 236 0.00
app/controllers/application_controller.rb 0.00 % 50 36 0 36 0.00
app/controllers/concerns/admin_authorizable.rb 0.00 % 95 70 0 70 0.00
app/controllers/concerns/api_response.rb 0.00 % 73 51 0 51 0.00
app/controllers/concerns/api_response_formatter.rb 0.00 % 235 182 0 182 0.00
app/controllers/concerns/api_security.rb 0.00 % 374 279 0 279 0.00
app/controllers/concerns/api_versionable.rb 0.00 % 223 151 0 151 0.00
app/controllers/concerns/authenticable.rb 0.00 % 36 26 0 26 0.00
app/controllers/concerns/commentable.rb 0.00 % 108 83 0 83 0.00
app/controllers/concerns/global_error_handler.rb 0.00 % 104 78 0 78 0.00
app/controllers/concerns/request_validator.rb 0.00 % 407 313 0 313 0.00
app/controllers/concerns/user_experience_enhancer.rb 0.00 % 324 236 0 236 0.00
app/jobs/application_job.rb 0.00 % 7 2 0 2 0.00
app/mailers/application_mailer.rb 0.00 % 4 4 0 4 0.00
app/models/application_record.rb 100.00 % 3 2 2 0 1.00
app/models/check_in.rb 40.49 % 382 163 66 97 0.40
app/models/comment.rb 31.67 % 149 60 19 41 0.87
app/models/content_report.rb 0.00 % 185 106 0 106 0.00
app/models/daily_flower_stat.rb 0.00 % 152 102 0 102 0.00
app/models/daily_leading.rb 0.00 % 63 47 0 47 0.00
app/models/enrollment.rb 50.00 % 53 26 13 13 0.50
app/models/event_enrollment.rb 36.99 % 416 146 54 92 0.37
app/models/flower.rb 46.34 % 111 41 19 22 0.46
app/models/flower_certificate.rb 42.00 % 128 50 21 29 0.42
app/models/flower_quota.rb 48.08 % 123 52 25 27 0.48
app/models/like.rb 71.11 % 113 45 32 13 5.58
app/models/notification.rb 41.56 % 219 77 32 45 0.44
app/models/participation_certificate.rb 0.00 % 297 211 0 211 0.00
app/models/post.rb 79.52 % 207 83 66 17 7.06
app/models/reading_event.rb 45.43 % 826 328 149 179 1.48
app/models/reading_schedule.rb 50.00 % 228 102 51 51 1.01
app/models/share_action.rb 0.00 % 162 125 0 125 0.00
app/models/user.rb 75.89 % 248 112 85 27 2.29
app/models/user_activity.rb 0.00 % 314 245 0 245 0.00
app/services/activity_approval_workflow_service.rb 0.00 % 725 539 0 539 0.00
app/services/analytics_service.rb 0.00 % 692 546 0 546 0.00
app/services/api_performance_service.rb 0.00 % 426 265 0 265 0.00
app/services/api_rate_limiting_service.rb 0.00 % 300 227 0 227 0.00
app/services/api_response_service.rb 0.00 % 297 182 0 182 0.00
app/services/api_version_service.rb 0.00 % 297 205 0 205 0.00
app/services/application_service.rb 62.50 % 102 48 30 18 0.92
app/services/authentication_service.rb 0.00 % 174 111 0 111 0.00
app/services/avatar_generator_service.rb 0.00 % 87 61 0 61 0.00
app/services/cache_service.rb 0.00 % 350 221 0 221 0.00
app/services/concerns/service_interface.rb 0.00 % 118 81 0 81 0.00
app/services/content_export_service.rb 0.00 % 510 398 0 398 0.00
app/services/content_formatter_service.rb 0.00 % 384 273 0 273 0.00
app/services/content_moderation_analytics_service.rb 0.00 % 285 213 0 213 0.00
app/services/content_moderation_query_service.rb 0.00 % 345 251 0 251 0.00
app/services/content_moderation_service.rb 0.00 % 258 202 0 202 0.00
app/services/content_search_service.rb 0.00 % 369 252 0 252 0.00
app/services/daily_flower_stats_service.rb 0.00 % 289 224 0 224 0.00
app/services/domain_events_service.rb 53.19 % 99 47 25 22 2.09
app/services/error_handling_service.rb 0.00 % 486 333 0 333 0.00
app/services/event_enrollment_service.rb 0.00 % 75 51 0 51 0.00
app/services/event_management_service.rb 0.00 % 141 99 0 99 0.00
app/services/event_subscribers/notification_event_subscriber.rb 18.06 % 179 72 13 59 0.18
app/services/flower_certificate_service.rb 0.00 % 239 178 0 178 0.00
app/services/flower_comment_service.rb 0.00 % 282 209 0 209 0.00
app/services/flower_giving_service.rb 0.00 % 181 141 0 141 0.00
app/services/flower_incentive_service.rb 0.00 % 193 122 0 122 0.00
app/services/flower_quota_service.rb 0.00 % 205 156 0 156 0.00
app/services/flower_statistics_service.rb 0.00 % 288 210 0 210 0.00
app/services/global_error_handler_service.rb 0.00 % 406 348 0 348 0.00
app/services/leader_assignment_service.rb 33.33 % 459 189 63 126 0.52
app/services/moderation_notification_service.rb 0.00 % 323 238 0 238 0.00
app/services/notification_service.rb 0.00 % 269 196 0 196 0.00
app/services/optimized_pagination_service.rb 0.00 % 213 161 0 161 0.00
app/services/pagination_service.rb 0.00 % 331 211 0 211 0.00
app/services/permission_check_service.rb 0.00 % 190 148 0 148 0.00
app/services/post_creation_service.rb 0.00 % 132 81 0 81 0.00
app/services/post_data_service.rb 0.00 % 476 338 0 338 0.00
app/services/post_management_service.rb 0.00 % 145 103 0 103 0.00
app/services/post_moderation_service.rb 0.00 % 229 157 0 157 0.00
app/services/post_permission_service.rb 0.00 % 281 186 0 186 0.00
app/services/post_service_facade.rb 0.00 % 209 158 0 158 0.00
app/services/post_update_service.rb 0.00 % 134 88 0 88 0.00
app/services/query_cache_service.rb 0.00 % 295 217 0 217 0.00
app/services/query_optimization_service.rb 0.00 % 272 158 0 158 0.00
app/services/report_creation_service.rb 0.00 % 239 156 0 156 0.00
app/services/report_processing_service.rb 0.00 % 232 163 0 163 0.00
app/services/response_optimization_service.rb 0.00 % 440 276 0 276 0.00
app/services/social_share_service.rb 0.00 % 432 337 0 337 0.00
app/services/user_activity_tracker.rb 0.00 % 414 322 0 322 0.00
app/services/user_experience_enhancer_service.rb 0.00 % 588 465 0 465 0.00

Controllers ( 0.0% covered at 0.0 hits/line )

41 files in total.
7800 relevant lines, 0 lines covered and 7800 lines missed. ( 0.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/api/admin/admin_controller.rb 0.00 % 188 157 0 157 0.00
app/controllers/api/application_controller.rb 0.00 % 179 137 0 137 0.00
app/controllers/api/auth_controller.rb 0.00 % 108 82 0 82 0.00
app/controllers/api/check_in_comments_controller.rb 0.00 % 42 34 0 34 0.00
app/controllers/api/check_ins_controller.rb 0.00 % 120 100 0 100 0.00
app/controllers/api/comments_controller.rb 0.00 % 26 20 0 20 0.00
app/controllers/api/daily_leadings_controller.rb 0.00 % 124 99 0 99 0.00
app/controllers/api/events_controller.rb 0.00 % 233 171 0 171 0.00
app/controllers/api/flowers_controller.rb 0.00 % 121 105 0 105 0.00
app/controllers/api/likes_controller.rb 0.00 % 56 46 0 46 0.00
app/controllers/api/optimized_posts_controller.rb 0.00 % 181 130 0 130 0.00
app/controllers/api/posts_controller.rb 0.00 % 199 142 0 142 0.00
app/controllers/api/uploads_controller.rb 0.00 % 55 36 0 36 0.00
app/controllers/api/v1/analytics_controller.rb 0.00 % 464 378 0 378 0.00
app/controllers/api/v1/approval_workflow_controller.rb 0.00 % 593 498 0 498 0.00
app/controllers/api/v1/base_controller.rb 0.00 % 302 231 0 231 0.00
app/controllers/api/v1/check_ins_controller.rb 0.00 % 727 590 0 590 0.00
app/controllers/api/v1/content_export_controller.rb 0.00 % 410 309 0 309 0.00
app/controllers/api/v1/content_reports_controller.rb 0.00 % 478 398 0 398 0.00
app/controllers/api/v1/content_search_controller.rb 0.00 % 359 269 0 269 0.00
app/controllers/api/v1/daily_leadings_controller.rb 0.00 % 278 224 0 224 0.00
app/controllers/api/v1/event_enrollments_controller.rb 0.00 % 343 264 0 264 0.00
app/controllers/api/v1/flower_comments_controller.rb 0.00 % 79 53 0 53 0.00
app/controllers/api/v1/flower_incentives_controller.rb 0.00 % 287 237 0 237 0.00
app/controllers/api/v1/flower_leaderboards_controller.rb 0.00 % 342 288 0 288 0.00
app/controllers/api/v1/leader_assignments_controller.rb 0.00 % 320 276 0 276 0.00
app/controllers/api/v1/notifications_controller.rb 0.00 % 203 145 0 145 0.00
app/controllers/api/v1/performance_posts_controller.rb 0.00 % 376 286 0 286 0.00
app/controllers/api/v1/reading_events_controller.rb 0.00 % 419 354 0 354 0.00
app/controllers/api/v1/reading_schedules_controller.rb 0.00 % 286 236 0 236 0.00
app/controllers/application_controller.rb 0.00 % 50 36 0 36 0.00
app/controllers/concerns/admin_authorizable.rb 0.00 % 95 70 0 70 0.00
app/controllers/concerns/api_response.rb 0.00 % 73 51 0 51 0.00
app/controllers/concerns/api_response_formatter.rb 0.00 % 235 182 0 182 0.00
app/controllers/concerns/api_security.rb 0.00 % 374 279 0 279 0.00
app/controllers/concerns/api_versionable.rb 0.00 % 223 151 0 151 0.00
app/controllers/concerns/authenticable.rb 0.00 % 36 26 0 26 0.00
app/controllers/concerns/commentable.rb 0.00 % 108 83 0 83 0.00
app/controllers/concerns/global_error_handler.rb 0.00 % 104 78 0 78 0.00
app/controllers/concerns/request_validator.rb 0.00 % 407 313 0 313 0.00
app/controllers/concerns/user_experience_enhancer.rb 0.00 % 324 236 0 236 0.00

Channels ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line

Models ( 29.86% covered at 0.93 hits/line )

20 files in total.
2123 relevant lines, 634 lines covered and 1489 lines missed. ( 29.86% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/models/application_record.rb 100.00 % 3 2 2 0 1.00
app/models/check_in.rb 40.49 % 382 163 66 97 0.40
app/models/comment.rb 31.67 % 149 60 19 41 0.87
app/models/content_report.rb 0.00 % 185 106 0 106 0.00
app/models/daily_flower_stat.rb 0.00 % 152 102 0 102 0.00
app/models/daily_leading.rb 0.00 % 63 47 0 47 0.00
app/models/enrollment.rb 50.00 % 53 26 13 13 0.50
app/models/event_enrollment.rb 36.99 % 416 146 54 92 0.37
app/models/flower.rb 46.34 % 111 41 19 22 0.46
app/models/flower_certificate.rb 42.00 % 128 50 21 29 0.42
app/models/flower_quota.rb 48.08 % 123 52 25 27 0.48
app/models/like.rb 71.11 % 113 45 32 13 5.58
app/models/notification.rb 41.56 % 219 77 32 45 0.44
app/models/participation_certificate.rb 0.00 % 297 211 0 211 0.00
app/models/post.rb 79.52 % 207 83 66 17 7.06
app/models/reading_event.rb 45.43 % 826 328 149 179 1.48
app/models/reading_schedule.rb 50.00 % 228 102 51 51 1.01
app/models/share_action.rb 0.00 % 162 125 0 125 0.00
app/models/user.rb 75.89 % 248 112 85 27 2.29
app/models/user_activity.rb 0.00 % 314 245 0 245 0.00

Mailers ( 0.0% covered at 0.0 hits/line )

1 files in total.
4 relevant lines, 0 lines covered and 4 lines missed. ( 0.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/mailers/application_mailer.rb 0.00 % 4 4 0 4 0.00

Helpers ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line

Jobs ( 0.0% covered at 0.0 hits/line )

1 files in total.
2 relevant lines, 0 lines covered and 2 lines missed. ( 0.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/jobs/application_job.rb 0.00 % 7 2 0 2 0.00

Libraries ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line

Ungrouped ( 1.23% covered at 0.02 hits/line )

51 files in total.
10613 relevant lines, 131 lines covered and 10482 lines missed. ( 1.23% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/services/activity_approval_workflow_service.rb 0.00 % 725 539 0 539 0.00
app/services/analytics_service.rb 0.00 % 692 546 0 546 0.00
app/services/api_performance_service.rb 0.00 % 426 265 0 265 0.00
app/services/api_rate_limiting_service.rb 0.00 % 300 227 0 227 0.00
app/services/api_response_service.rb 0.00 % 297 182 0 182 0.00
app/services/api_version_service.rb 0.00 % 297 205 0 205 0.00
app/services/application_service.rb 62.50 % 102 48 30 18 0.92
app/services/authentication_service.rb 0.00 % 174 111 0 111 0.00
app/services/avatar_generator_service.rb 0.00 % 87 61 0 61 0.00
app/services/cache_service.rb 0.00 % 350 221 0 221 0.00
app/services/concerns/service_interface.rb 0.00 % 118 81 0 81 0.00
app/services/content_export_service.rb 0.00 % 510 398 0 398 0.00
app/services/content_formatter_service.rb 0.00 % 384 273 0 273 0.00
app/services/content_moderation_analytics_service.rb 0.00 % 285 213 0 213 0.00
app/services/content_moderation_query_service.rb 0.00 % 345 251 0 251 0.00
app/services/content_moderation_service.rb 0.00 % 258 202 0 202 0.00
app/services/content_search_service.rb 0.00 % 369 252 0 252 0.00
app/services/daily_flower_stats_service.rb 0.00 % 289 224 0 224 0.00
app/services/domain_events_service.rb 53.19 % 99 47 25 22 2.09
app/services/error_handling_service.rb 0.00 % 486 333 0 333 0.00
app/services/event_enrollment_service.rb 0.00 % 75 51 0 51 0.00
app/services/event_management_service.rb 0.00 % 141 99 0 99 0.00
app/services/event_subscribers/notification_event_subscriber.rb 18.06 % 179 72 13 59 0.18
app/services/flower_certificate_service.rb 0.00 % 239 178 0 178 0.00
app/services/flower_comment_service.rb 0.00 % 282 209 0 209 0.00
app/services/flower_giving_service.rb 0.00 % 181 141 0 141 0.00
app/services/flower_incentive_service.rb 0.00 % 193 122 0 122 0.00
app/services/flower_quota_service.rb 0.00 % 205 156 0 156 0.00
app/services/flower_statistics_service.rb 0.00 % 288 210 0 210 0.00
app/services/global_error_handler_service.rb 0.00 % 406 348 0 348 0.00
app/services/leader_assignment_service.rb 33.33 % 459 189 63 126 0.52
app/services/moderation_notification_service.rb 0.00 % 323 238 0 238 0.00
app/services/notification_service.rb 0.00 % 269 196 0 196 0.00
app/services/optimized_pagination_service.rb 0.00 % 213 161 0 161 0.00
app/services/pagination_service.rb 0.00 % 331 211 0 211 0.00
app/services/permission_check_service.rb 0.00 % 190 148 0 148 0.00
app/services/post_creation_service.rb 0.00 % 132 81 0 81 0.00
app/services/post_data_service.rb 0.00 % 476 338 0 338 0.00
app/services/post_management_service.rb 0.00 % 145 103 0 103 0.00
app/services/post_moderation_service.rb 0.00 % 229 157 0 157 0.00
app/services/post_permission_service.rb 0.00 % 281 186 0 186 0.00
app/services/post_service_facade.rb 0.00 % 209 158 0 158 0.00
app/services/post_update_service.rb 0.00 % 134 88 0 88 0.00
app/services/query_cache_service.rb 0.00 % 295 217 0 217 0.00
app/services/query_optimization_service.rb 0.00 % 272 158 0 158 0.00
app/services/report_creation_service.rb 0.00 % 239 156 0 156 0.00
app/services/report_processing_service.rb 0.00 % 232 163 0 163 0.00
app/services/response_optimization_service.rb 0.00 % 440 276 0 276 0.00
app/services/social_share_service.rb 0.00 % 432 337 0 337 0.00
app/services/user_activity_tracker.rb 0.00 % 414 322 0 322 0.00
app/services/user_experience_enhancer_service.rb 0.00 % 588 465 0 465 0.00

app/controllers/api/admin/admin_controller.rb

0.0% lines covered

157 relevant lines. 0 lines covered and 157 lines missed.
    
  1. module Api
  2. module Admin
  3. class AdminController < ApplicationController
  4. include AdminAuthorizable
  5. before_action :authenticate_admin!
  6. # GET /api/admin/dashboard
  7. def dashboard
  8. render json: {
  9. current_user: {
  10. id: current_user.id,
  11. nickname: current_user.nickname,
  12. role: current_user.role_display_name,
  13. permissions: current_user_permissions
  14. },
  15. system_stats: {
  16. total_users: User.count,
  17. total_posts: Post.count,
  18. visible_posts: Post.visible.count,
  19. total_events: ReadingEvent.count,
  20. pending_events: ReadingEvent.where(approval_status: :pending).count,
  21. active_events: ReadingEvent.where(status: :in_progress).count,
  22. admin_count: User.where(role: :admin).count,
  23. root_count: User.where(role: :root).count
  24. },
  25. available_actions: admin_available_actions
  26. }
  27. end
  28. # GET /api/admin/users
  29. def users
  30. authenticate_root! # 只有root可以查看所有用户
  31. users = User.select(:id, :nickname, :role, :created_at, :wx_openid)
  32. .order(created_at: :desc)
  33. render json: {
  34. users: users.map { |user|
  35. {
  36. id: user.id,
  37. nickname: user.nickname,
  38. role: user.role_display_name,
  39. role_value: user.role,
  40. created_at: user.created_at,
  41. permissions: user_permissions_for(user)
  42. }
  43. },
  44. summary: {
  45. total: users.count,
  46. by_role: {
  47. user: users.select(&:user?).count,
  48. admin: users.select(&:admin?).count,
  49. root: users.select(&:root?).count
  50. }
  51. }
  52. }
  53. end
  54. # PUT /api/admin/users/:id/promote_admin
  55. def promote_user_to_admin
  56. authenticate_root! # 只有root可以提升管理员
  57. user = User.find(params[:id])
  58. if user.root?
  59. return render json: { error: "不能提升超级管理员" }, status: :unprocessable_entity
  60. end
  61. if user.update!(role: :admin)
  62. render json: {
  63. message: "用户已提升为管理员",
  64. user: {
  65. id: user.id,
  66. nickname: user.nickname,
  67. new_role: user.role_display_name
  68. }
  69. }
  70. else
  71. render json: { error: "提升失败" }, status: :unprocessable_entity
  72. end
  73. end
  74. # PUT /api/admin/users/:id/demote
  75. def demote_user
  76. authenticate_root! # 只有root可以降级用户
  77. user = User.find(params[:id])
  78. if user.root?
  79. return render json: { error: "不能降级超级管理员" }, status: :unprocessable_entity
  80. end
  81. if user.update!(role: :participant)
  82. render json: {
  83. message: "用户已降级为参与者",
  84. user: {
  85. id: user.id,
  86. nickname: user.nickname,
  87. new_role: user.role_display_name
  88. }
  89. }
  90. else
  91. render json: { error: "降级失败" }, status: :unprocessable_entity
  92. end
  93. end
  94. # GET /api/admin/events/pending
  95. def pending_events
  96. events = ReadingEvent.includes(:leader)
  97. .where(approval_status: :pending)
  98. .order(created_at: :desc)
  99. render json: {
  100. events: events.map { |event|
  101. {
  102. id: event.id,
  103. title: event.title,
  104. book_name: event.book_name,
  105. leader: {
  106. id: event.leader.id,
  107. nickname: event.leader.nickname
  108. },
  109. created_at: event.created_at,
  110. enrollment_fee: event.enrollment_fee,
  111. max_participants: event.max_participants
  112. }
  113. },
  114. count: events.count
  115. }
  116. end
  117. # POST /api/admin/init_root
  118. def init_root_user
  119. # 这个接口用于系统初始化时创建root用户
  120. # 应该在系统部署后立即调用,然后禁用
  121. if User.exists?(role: :root)
  122. return render json: { error: "Root用户已存在" }, status: :unprocessable_entity
  123. end
  124. # 这里应该有更严格的验证,比如特定的token或者IP限制
  125. # 为了演示,这里简化处理
  126. root_info = params.require(:root).permit(:wx_openid, :nickname, :avatar_url)
  127. user = User.new(root_info)
  128. user.role = :root
  129. if user.save
  130. render json: {
  131. message: "Root用户创建成功",
  132. user: {
  133. id: user.id,
  134. nickname: user.nickname,
  135. role: user.role_display_name
  136. }
  137. }
  138. else
  139. render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
  140. end
  141. end
  142. private
  143. def current_user_permissions
  144. [
  145. "approve_events",
  146. "view_admin_panel"
  147. ].select { |perm| current_user.has_permission?(perm.to_sym) }
  148. end
  149. def admin_available_actions
  150. actions = []
  151. actions << { action: "approve_events", description: "审批活动" } if current_user.can_approve_events?
  152. actions << { action: "manage_users", description: "管理用户" } if current_user.can_manage_users?
  153. actions << { action: "view_admin_panel", description: "查看管理面板" } if current_user.can_view_admin_panel?
  154. actions << { action: "manage_system", description: "管理系统" } if current_user.can_manage_system?
  155. actions
  156. end
  157. def user_permissions_for(user)
  158. permissions = []
  159. permissions << "approve_events" if user.can_approve_events?
  160. permissions << "manage_users" if user.can_manage_users?
  161. permissions << "view_admin_panel" if user.can_view_admin_panel?
  162. permissions << "manage_system" if user.can_manage_system?
  163. permissions
  164. end
  165. end
  166. end
  167. end

app/controllers/api/application_controller.rb

0.0% lines covered

137 relevant lines. 0 lines covered and 137 lines missed.
    
  1. # frozen_string_literal: true
  2. module Api
  3. class ApplicationController < ActionController::API
  4. # 简单的健康检查
  5. def health
  6. render json: {
  7. status: "ok",
  8. timestamp: Time.current.iso8601,
  9. environment: Rails.env,
  10. version: "1.0.0"
  11. }
  12. end
  13. private
  14. # 从 JWT token 中获取当前用户
  15. def current_user
  16. return unless auth_header_present?
  17. return unless auth_token_valid?
  18. user_id = decoded_jwt_token['user_id']
  19. @current_user ||= User.find_by(id: user_id)
  20. end
  21. # 检查是否需要用户认证
  22. def authenticate_user!
  23. return if current_user
  24. # 提供更详细的错误信息用于调试
  25. error_info = determine_auth_error
  26. Rails.logger.warn "认证失败: #{error_info[:reason]} - #{error_info[:details]}"
  27. render json: {
  28. error: error_info[:message],
  29. error_code: error_info[:code],
  30. details: Rails.env.development? ? error_info[:details] : nil
  31. }, status: :unauthorized
  32. end
  33. private
  34. def auth_header_present?
  35. request.headers['Authorization'].present?
  36. end
  37. def auth_token_valid?
  38. auth_header = request.headers['Authorization']
  39. token = auth_header.split(' ').last if auth_header
  40. return false unless token
  41. decoded_token = User.decode_jwt_token(token)
  42. return false unless decoded_token
  43. # 检查 token 是否过期
  44. Time.current < Time.at(decoded_token['exp'])
  45. end
  46. def decoded_jwt_token
  47. auth_header = request.headers['Authorization']
  48. token = auth_header.split(' ').last if auth_header
  49. @decoded_jwt_token ||= User.decode_jwt_token(token) if token
  50. end
  51. # 分析认证失败的具体原因
  52. def determine_auth_error
  53. auth_header = request.headers['Authorization']
  54. # 1. 检查是否有Authorization头
  55. unless auth_header.present?
  56. return {
  57. code: 'MISSING_AUTH_HEADER',
  58. message: '缺少认证信息',
  59. reason: 'no_auth_header',
  60. details: '请求头中缺少Authorization字段'
  61. }
  62. end
  63. # 2. 检查Authorization格式
  64. unless auth_header.start_with?('Bearer ')
  65. return {
  66. code: 'INVALID_AUTH_FORMAT',
  67. message: '认证格式错误',
  68. reason: 'invalid_auth_format',
  69. details: "Authorization头格式应为'Bearer <token>',当前为: #{auth_header[0..50]}..."
  70. }
  71. end
  72. # 3. 提取token
  73. token = auth_header.split(' ').last
  74. unless token.present?
  75. return {
  76. code: 'MISSING_TOKEN',
  77. message: '缺少认证令牌',
  78. reason: 'missing_token',
  79. details: 'Authorization头中缺少token部分'
  80. }
  81. end
  82. # 4. 检查token格式
  83. unless token.include?('.')
  84. return {
  85. code: 'INVALID_TOKEN_FORMAT',
  86. message: '令牌格式错误',
  87. reason: 'invalid_token_format',
  88. details: 'JWT token应包含三个部分,用点分隔'
  89. }
  90. end
  91. # 5. 尝试解码token
  92. begin
  93. decoded = User.decode_jwt_token(token)
  94. unless decoded
  95. return {
  96. code: 'INVALID_TOKEN',
  97. message: '令牌无效',
  98. reason: 'decode_failed',
  99. details: 'JWT token解码失败,可能被篡改或格式错误'
  100. }
  101. end
  102. # 6. 检查token是否过期
  103. exp_time = decoded['exp']
  104. if exp_time
  105. current_time = Time.current.to_i
  106. if current_time >= exp_time
  107. expired_time = Time.at(exp_time)
  108. return {
  109. code: 'TOKEN_EXPIRED',
  110. message: '令牌已过期',
  111. reason: 'token_expired',
  112. details: "Token已于#{expired_time.strftime('%Y-%m-%d %H:%M:%S')}过期"
  113. }
  114. end
  115. end
  116. # 7. 检查用户是否存在
  117. user_id = decoded['user_id']
  118. unless user_id
  119. return {
  120. code: 'INVALID_TOKEN_PAYLOAD',
  121. message: '令牌内容无效',
  122. reason: 'missing_user_id',
  123. details: 'Token中缺少user_id字段'
  124. }
  125. end
  126. user = User.find_by(id: user_id)
  127. unless user
  128. return {
  129. code: 'USER_NOT_FOUND',
  130. message: '用户不存在',
  131. reason: 'user_not_found',
  132. details: "Token中用户ID(#{user_id})对应的用户不存在"
  133. }
  134. end
  135. # 8. 检查用户状态(如果需要)
  136. # 这里可以添加用户状态检查逻辑
  137. rescue => e
  138. return {
  139. code: 'TOKEN_PROCESSING_ERROR',
  140. message: '令牌处理错误',
  141. reason: 'processing_error',
  142. details: "处理token时发生错误: #{e.message}"
  143. }
  144. end
  145. # 未知错误
  146. {
  147. code: 'UNKNOWN_AUTH_ERROR',
  148. message: '认证失败',
  149. reason: 'unknown',
  150. details: '认证过程中发生未知错误'
  151. }
  152. end
  153. end
  154. end

app/controllers/api/auth_controller.rb

0.0% lines covered

82 relevant lines. 0 lines covered and 82 lines missed.
    
  1. module Api
  2. class AuthController < Api::ApplicationController
  3. before_action :authenticate_user!, only: [:me, :update_profile]
  4. # 引入Service
  5. def authentication_service
  6. AuthenticationService
  7. end
  8. # 模拟登录(测试用)
  9. def mock_login
  10. # 使用AuthenticationService处理模拟登录
  11. service_result = authentication_service.mock_login!(params.to_unsafe_h)
  12. if service_result.success?
  13. render json: service_result.result
  14. else
  15. render json: { error: service_result.first_error }, status: :unprocessable_entity
  16. end
  17. end
  18. # 微信登录(新版,支持用户信息传递)
  19. def wechat_login
  20. # 使用AuthenticationService处理微信登录,支持传递用户信息
  21. service_result = authentication_service.wechat_login!(params.to_unsafe_h)
  22. if service_result.success?
  23. render json: service_result.result
  24. else
  25. error_message = service_result.first_error
  26. status_code = error_message.include?("code") ? :bad_request : :unauthorized
  27. render json: { error: error_message }, status: status_code
  28. end
  29. end
  30. # 微信登录(生产用)
  31. def login
  32. # 使用AuthenticationService处理微信登录
  33. service_result = authentication_service.wechat_login!(params.to_unsafe_h)
  34. if service_result.success?
  35. render json: service_result.result
  36. else
  37. error_message = service_result.first_error
  38. status_code = error_message.include?("code") ? :bad_request : :unauthorized
  39. render json: { error: error_message }, status: status_code
  40. end
  41. end
  42. # 获取当前用户信息
  43. def me
  44. render json: {
  45. user: current_user.as_json_for_api
  46. }
  47. end
  48. # 刷新访问令牌
  49. def refresh_token
  50. refresh_token_param = params[:refresh_token]
  51. unless refresh_token_param.present?
  52. return render json: {
  53. error: '缺少refresh_token参数',
  54. error_code: 'MISSING_REFRESH_TOKEN'
  55. }, status: :bad_request
  56. end
  57. result = User.refresh_access_token(refresh_token_param)
  58. if result
  59. render json: {
  60. message: 'Token刷新成功',
  61. access_token: result[:access_token],
  62. refresh_token: result[:refresh_token],
  63. user: result[:user]
  64. }
  65. else
  66. render json: {
  67. error: 'refresh_token无效或已过期',
  68. error_code: 'INVALID_REFRESH_TOKEN'
  69. }, status: :unauthorized
  70. end
  71. end
  72. # 更新用户资料
  73. def update_profile
  74. if current_user.update(profile_params)
  75. render json: {
  76. message: "更新成功",
  77. user: {
  78. id: current_user.id,
  79. nickname: current_user.nickname,
  80. avatar_url: current_user.avatar_url,
  81. phone: current_user.phone
  82. }
  83. }
  84. else
  85. render json: { errors: current_user.errors.full_messages }, status: :unprocessable_entity
  86. end
  87. end
  88. # fetch_wechat_openid方法已移至AuthenticationService
  89. def profile_params
  90. params.require(:user).permit(:nickname, :avatar_url, :phone)
  91. end
  92. end
  93. end

app/controllers/api/check_in_comments_controller.rb

0.0% lines covered

34 relevant lines. 0 lines covered and 34 lines missed.
    
  1. module Api
  2. class CheckInCommentsController < Api::ApplicationController
  3. include Commentable
  4. before_action :set_check_in, only: [:index, :create]
  5. private
  6. def fetch_comments
  7. @check_in.comments
  8. end
  9. def build_comment(params)
  10. comment = @check_in.comments.new(params)
  11. # 打卡评论不需要post_id
  12. comment.post_id = nil
  13. comment
  14. end
  15. def format_single_comment(comment, can_edit = false)
  16. # 为打卡评论使用专门的JSON格式,保持兼容性
  17. {
  18. id: comment.id,
  19. content: comment.content,
  20. created_at: comment.created_at,
  21. updated_at: comment.updated_at,
  22. author_info: {
  23. id: comment.user.id,
  24. nickname: comment.user.nickname,
  25. avatar_url: comment.user.avatar_url
  26. },
  27. can_edit_current_user: can_edit || can_edit_comment?(comment, current_user)
  28. }
  29. end
  30. def set_check_in
  31. @check_in = CheckIn.find(params[:check_in_id])
  32. rescue ActiveRecord::RecordNotFound
  33. render_not_found('打卡不存在')
  34. end
  35. end
  36. end

app/controllers/api/check_ins_controller.rb

0.0% lines covered

100 relevant lines. 0 lines covered and 100 lines missed.
    
  1. class Api::CheckInsController < ApplicationController
  2. include Authenticable
  3. # POST /api/reading_schedules/:reading_schedule_id/check_ins
  4. def create
  5. reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
  6. enrollment = current_user.enrollments.find_by(reading_event: reading_schedule.reading_event)
  7. unless enrollment
  8. return render json: { error: "未报名该活动" }, status: :unprocessable_entity
  9. end
  10. check_in = CheckIn.new(check_in_params)
  11. check_in.user = current_user
  12. check_in.reading_schedule = reading_schedule
  13. check_in.enrollment = enrollment
  14. if check_in.save
  15. render json: check_in, status: :created
  16. else
  17. render json: { error: check_in.errors.full_messages }, status: :unprocessable_entity
  18. end
  19. end
  20. # GET /api/reading_schedules/:reading_schedule_id/check_ins
  21. def index
  22. reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
  23. check_ins = reading_schedule.check_ins.includes(:user, :flower)
  24. render json: check_ins.map { |ci|
  25. {
  26. id: ci.id,
  27. user: {
  28. id: ci.user.id,
  29. nickname: ci.user.nickname,
  30. avatar_url: ci.user.avatar_url
  31. },
  32. content: ci.content,
  33. word_count: ci.word_count,
  34. status: ci.status,
  35. submitted_at: ci.submitted_at,
  36. has_flower: ci.has_flower?,
  37. flower: ci.flower ? {
  38. giver: {
  39. id: ci.flower.giver.id,
  40. nickname: ci.flower.giver.nickname
  41. },
  42. comment: ci.flower.comment
  43. } : nil
  44. }
  45. }
  46. end
  47. # GET /api/check_ins/:id
  48. def show
  49. check_in = CheckIn.includes(:user, :reading_schedule, :flower).find(params[:id])
  50. render json: {
  51. id: check_in.id,
  52. user: {
  53. id: check_in.user.id,
  54. nickname: check_in.user.nickname,
  55. avatar_url: check_in.user.avatar_url
  56. },
  57. reading_schedule: {
  58. id: check_in.reading_schedule.id,
  59. day_number: check_in.reading_schedule.day_number,
  60. date: check_in.reading_schedule.date,
  61. reading_progress: check_in.reading_schedule.reading_progress
  62. },
  63. content: check_in.content,
  64. word_count: check_in.word_count,
  65. status: check_in.status,
  66. submitted_at: check_in.submitted_at,
  67. updated_at: check_in.updated_at,
  68. has_flower: check_in.has_flower?,
  69. flower: check_in.flower ? {
  70. giver: {
  71. id: check_in.flower.giver.id,
  72. nickname: check_in.flower.giver.nickname
  73. },
  74. comment: check_in.flower.comment,
  75. created_at: check_in.flower.created_at
  76. } : nil
  77. }
  78. end
  79. # PUT /api/check_ins/:id
  80. def update
  81. check_in = CheckIn.find(params[:id])
  82. # 只能修改自己的打卡
  83. unless check_in.user_id == current_user.id
  84. return render json: { error: "只能修改自己的打卡" }, status: :forbidden
  85. end
  86. # 如果已经获得小红花,不允许修改
  87. if check_in.has_flower?
  88. return render json: { error: "已获得小红花的打卡不允许修改" }, status: :unprocessable_entity
  89. end
  90. if check_in.update(check_in_params)
  91. render json: {
  92. id: check_in.id,
  93. content: check_in.content,
  94. word_count: check_in.word_count,
  95. status: check_in.status,
  96. updated_at: check_in.updated_at
  97. }
  98. else
  99. render json: { errors: check_in.errors.full_messages }, status: :unprocessable_entity
  100. end
  101. end
  102. private
  103. def check_in_params
  104. params.require(:check_in).permit(:content, :status)
  105. end
  106. end

app/controllers/api/comments_controller.rb

0.0% lines covered

20 relevant lines. 0 lines covered and 20 lines missed.
    
  1. module Api
  2. class CommentsController < Api::ApplicationController
  3. include Commentable
  4. before_action :set_post, only: [:index, :create]
  5. private
  6. def fetch_comments
  7. @post.comments
  8. end
  9. def build_comment(params)
  10. comment = @post.comments.new(params)
  11. # 设置 commentable 关联
  12. comment.commentable = @post
  13. comment
  14. end
  15. def set_post
  16. @post = Post.find(params[:post_id])
  17. rescue ActiveRecord::RecordNotFound
  18. render_not_found('帖子不存在')
  19. end
  20. end
  21. end

app/controllers/api/daily_leadings_controller.rb

0.0% lines covered

99 relevant lines. 0 lines covered and 99 lines missed.
    
  1. class Api::DailyLeadingsController < ApplicationController
  2. include Authenticable
  3. skip_before_action :authenticate_user!, only: [:show]
  4. # POST /api/reading_schedules/:reading_schedule_id/daily_leading
  5. def create
  6. reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
  7. event = reading_schedule.reading_event
  8. # 检查活动是否进行中
  9. unless event.in_progress?
  10. return render json: { error: "活动未开始或已结束" }, status: :unprocessable_entity
  11. end
  12. # 检查是否有权限发布领读内容(前一天权限 + 小组长补位)
  13. unless event.can_publish_leading_content?(current_user, reading_schedule)
  14. return render json: {
  15. error: "只有领读人或小组长可以发布领读内容",
  16. details: {
  17. leader_permission: "领读人可提前一天或当天发布",
  18. group_leader_permission: "小组长全程具备发布权限(补位机制)",
  19. current_user_role: event.current_leader?(current_user) ? "小组长" : "普通用户",
  20. is_schedule_leader: reading_schedule.daily_leader_id == current_user.id
  21. }
  22. }, status: :forbidden
  23. end
  24. daily_leading = reading_schedule.build_daily_leading(daily_leading_params)
  25. daily_leading.leader = current_user
  26. if daily_leading.save
  27. render json: {
  28. id: daily_leading.id,
  29. leader: {
  30. id: daily_leading.leader.id,
  31. nickname: daily_leading.leader.nickname,
  32. avatar_url: daily_leading.leader.avatar_url
  33. },
  34. reading_suggestion: daily_leading.reading_suggestion,
  35. questions: daily_leading.questions,
  36. schedule_date: reading_schedule.date,
  37. publish_window: "可提前一天或当天发布",
  38. created_at: daily_leading.created_at
  39. }, status: :created
  40. else
  41. render json: { error: daily_leading.errors.full_messages }, status: :unprocessable_entity
  42. end
  43. end
  44. # GET /api/reading_schedules/:reading_schedule_id/daily_leading
  45. def show
  46. reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
  47. daily_leading = reading_schedule.daily_leading
  48. if daily_leading
  49. render json: {
  50. id: daily_leading.id,
  51. leader: {
  52. id: daily_leading.leader.id,
  53. nickname: daily_leading.leader.nickname,
  54. avatar_url: daily_leading.leader.avatar_url
  55. },
  56. reading_suggestion: daily_leading.reading_suggestion,
  57. questions: daily_leading.questions,
  58. created_at: daily_leading.created_at
  59. }
  60. else
  61. render json: { error: "今日暂无领读内容" }, status: :not_found
  62. end
  63. end
  64. # PUT /api/reading_schedules/:reading_schedule_id/daily_leading
  65. def update
  66. reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
  67. daily_leading = reading_schedule.daily_leading
  68. event = reading_schedule.reading_event
  69. unless daily_leading
  70. return render json: { error: "领读内容不存在" }, status: :not_found
  71. end
  72. # 检查活动是否进行中
  73. unless event.in_progress?
  74. return render json: { error: "活动未开始或已结束" }, status: :unprocessable_entity
  75. end
  76. # 检查权限:原作者或当前有效的小组长
  77. is_original_author = daily_leading.leader_id == current_user.id
  78. is_current_leader = event.current_leader?(current_user)
  79. is_today_leader = event.current_daily_leader?(current_user, reading_schedule)
  80. unless is_original_author || is_current_leader || is_today_leader
  81. return render json: { error: "无权限修改该内容" }, status: :forbidden
  82. end
  83. # 如果当天已经有打卡,不建议修改领读内容
  84. if reading_schedule.check_ins.any?
  85. return render json: { error: "已有用户打卡,不建议修改领读内容" }, status: :unprocessable_entity
  86. end
  87. if daily_leading.update(daily_leading_params)
  88. render json: {
  89. id: daily_leading.id,
  90. leader: {
  91. id: daily_leading.leader.id,
  92. nickname: daily_leading.leader.nickname,
  93. avatar_url: daily_leading.leader.avatar_url
  94. },
  95. reading_suggestion: daily_leading.reading_suggestion,
  96. questions: daily_leading.questions,
  97. updated_at: daily_leading.updated_at
  98. }
  99. else
  100. render json: { error: daily_leading.errors.full_messages }, status: :unprocessable_entity
  101. end
  102. end
  103. private
  104. def daily_leading_params
  105. params.require(:daily_leading).permit(:reading_suggestion, :questions)
  106. end
  107. end

app/controllers/api/events_controller.rb

0.0% lines covered

171 relevant lines. 0 lines covered and 171 lines missed.
    
  1. module Api
  2. class EventsController < Api::ApplicationController
  3. skip_before_action :authenticate_user!, only: [:index, :show], raise: false
  4. include AdminAuthorizable
  5. # GET /api/events
  6. def index
  7. @events = ReadingEvent.includes(:leader).order(created_at: :desc)
  8. # 筛选条件
  9. @events = @events.where(status: params[:status]) if params[:status].present?
  10. render json: @events.map { |event| event_json(event) }
  11. end
  12. # GET /api/events/:id
  13. def show
  14. @event = ReadingEvent.includes(:leader, :participants).find(params[:id])
  15. render json: event_detail_json(@event)
  16. end
  17. # POST /api/events
  18. def create
  19. @event = current_user.created_events.new(event_params)
  20. @event.leader = current_user
  21. @event.approval_status = :pending # 新活动默认待审批
  22. if @event.save
  23. # 自动生成阅读计划
  24. generate_reading_schedules(@event)
  25. render json: event_json(@event), status: :created
  26. else
  27. render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
  28. end
  29. end
  30. # PUT /api/events/:id
  31. def update
  32. @event = current_user.created_events.find(params[:id])
  33. if @event.update(event_params)
  34. render json: event_json(@event)
  35. else
  36. render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
  37. end
  38. end
  39. # DELETE /api/events/:id
  40. def destroy
  41. @event = current_user.created_events.find(params[:id])
  42. @event.destroy
  43. head :no_content
  44. end
  45. # POST /api/events/:id/enroll
  46. def enroll
  47. @event = ReadingEvent.find(params[:id])
  48. # 使用EventEnrollmentService处理报名
  49. service_result = EventEnrollmentService.enroll_user!(@event, current_user)
  50. if service_result.success?
  51. render json: service_result.result, status: :created
  52. else
  53. render json: { error: service_result.first_error }, status: :unprocessable_entity
  54. end
  55. end
  56. # POST /api/events/:id/approve
  57. def approve
  58. @event = ReadingEvent.find(params[:id])
  59. # 检查是否有审批权限
  60. authorize_event_approval!
  61. # 使用EventManagementService处理审批
  62. service_result = EventManagementService.approve_event!(@event, current_user)
  63. if service_result.success?
  64. render json: service_result.result
  65. else
  66. render json: { error: service_result.first_error }, status: :unprocessable_entity
  67. end
  68. end
  69. # POST /api/events/:id/reject
  70. def reject
  71. @event = ReadingEvent.find(params[:id])
  72. # 检查是否有审批权限
  73. authorize_event_approval!
  74. # 使用EventManagementService处理拒绝
  75. service_result = EventManagementService.reject_event!(@event, current_user)
  76. if service_result.success?
  77. render json: service_result.result
  78. else
  79. render json: { error: service_result.first_error }, status: :unprocessable_entity
  80. end
  81. end
  82. # POST /api/events/:id/claim_leadership
  83. def claim_leadership
  84. @event = ReadingEvent.find(params[:id])
  85. schedule = @event.reading_schedules.find(params[:schedule_id])
  86. # 使用LeaderAssignmentService处理领读报名
  87. service_result = LeaderAssignmentService.claim_leadership!(@event, current_user, schedule)
  88. if service_result.success?
  89. render json: service_result.result
  90. else
  91. render json: { error: service_result.first_error }, status: :unprocessable_entity
  92. end
  93. end
  94. # POST /api/events/:id/complete
  95. def complete
  96. @event = ReadingEvent.find(params[:id])
  97. # 使用EventManagementService处理活动完成
  98. service_result = EventManagementService.complete_event!(@event, current_user)
  99. if service_result.success?
  100. render json: service_result.result
  101. else
  102. status_code = service_result.first_error.include?("只有") ? :forbidden : :unprocessable_entity
  103. render json: { error: service_result.first_error }, status: status_code
  104. end
  105. end
  106. # GET /api/events/:id/backup_needed
  107. def backup_needed
  108. @event = ReadingEvent.find(params[:id])
  109. # 检查是否是当前有效的小组长
  110. unless @event.current_leader?(current_user)
  111. return render json: { error: "只有活动小组长可以查看补位信息" }, status: :forbidden
  112. end
  113. backup_schedules = @event.schedules_need_backup
  114. render json: {
  115. event_id: @event.id,
  116. event_title: @event.title,
  117. backup_needed: backup_schedules,
  118. summary: {
  119. total_needing_backup: backup_schedules.count,
  120. missing_content_count: backup_schedules.count { |s| s[:missing_content] },
  121. missing_flowers_count: backup_schedules.count { |s| s[:missing_flowers] },
  122. urgent_count: backup_schedules.count { |s| s[:date] <= Date.today }
  123. },
  124. leader_permissions: {
  125. can_publish_content: true,
  126. can_give_flowers: true,
  127. backup_mechanism: "小组长全程具备领读权限,可随时补位"
  128. }
  129. }
  130. end
  131. private
  132. def event_params
  133. params.require(:event).permit(
  134. :title, :book_name, :book_cover_url, :description,
  135. :start_date, :end_date, :max_participants, :enrollment_fee, :status,
  136. :leader_assignment_type
  137. )
  138. end
  139. def event_json(event)
  140. {
  141. id: event.id,
  142. title: event.title,
  143. book_name: event.book_name,
  144. book_cover_url: event.book_cover_url,
  145. description: event.description,
  146. start_date: event.start_date,
  147. end_date: event.end_date,
  148. max_participants: event.max_participants,
  149. enrollment_fee: event.enrollment_fee,
  150. service_fee: event.service_fee,
  151. deposit: event.deposit,
  152. status: event.status,
  153. approval_status: event.approval_status,
  154. leader_assignment_type: event.leader_assignment_type,
  155. days_count: event.days_count,
  156. leader: {
  157. id: event.leader.id,
  158. nickname: event.leader.nickname,
  159. avatar_url: event.leader.avatar_url
  160. },
  161. approved_by: event.approved_by ? {
  162. id: event.approved_by.id,
  163. nickname: event.approved_by.nickname
  164. } : nil,
  165. approved_at: event.approved_at,
  166. created_at: event.created_at
  167. }
  168. end
  169. def event_detail_json(event)
  170. event_json(event).merge(
  171. participants_count: event.participants.count,
  172. participants: event.participants.map { |user|
  173. {
  174. id: user.id,
  175. nickname: user.nickname,
  176. avatar_url: user.avatar_url
  177. }
  178. }
  179. )
  180. end
  181. # enrollment_json方法已移至EventEnrollmentService
  182. def generate_reading_schedules(event)
  183. days_count = event.days_count
  184. days_count.times do |i|
  185. event.reading_schedules.create!(
  186. day_number: i + 1,
  187. date: event.start_date + i.days,
  188. reading_progress: "第 #{i + 1} 天阅读计划(待领读人填写)"
  189. )
  190. end
  191. end
  192. end
  193. end

app/controllers/api/flowers_controller.rb

0.0% lines covered

105 relevant lines. 0 lines covered and 105 lines missed.
    
  1. class Api::FlowersController < ApplicationController
  2. include Authenticable
  3. # POST /api/check_ins/:check_in_id/flower
  4. def create
  5. check_in = CheckIn.find(params[:check_in_id])
  6. reading_schedule = check_in.reading_schedule
  7. event = reading_schedule.reading_event
  8. # 不能给自己的打卡送花
  9. if check_in.user_id == current_user.id
  10. return render json: { error: "不能给自己送小红花" }, status: :unprocessable_entity
  11. end
  12. # 检查是否有权限发放小红花(当天和后一天权限 + 小组长补位)
  13. unless event.can_give_flowers?(current_user, reading_schedule)
  14. return render json: {
  15. error: "只有领读人或小组长可以发放小红花",
  16. details: {
  17. leader_permission: "领读人可在当天或后一天发放",
  18. group_leader_permission: "小组长全程具备发放权限(补位机制)",
  19. current_user_role: event.current_leader?(current_user) ? "小组长" : "普通用户",
  20. flower_window: "小红花发放窗口灵活,支持补位机制"
  21. }
  22. }, status: :forbidden
  23. end
  24. flower = Flower.new(flower_params)
  25. flower.check_in = check_in
  26. flower.giver = current_user
  27. flower.recipient = check_in.user
  28. flower.reading_schedule = reading_schedule
  29. if flower.save
  30. render json: {
  31. id: flower.id,
  32. check_in_id: flower.check_in_id,
  33. giver: {
  34. id: flower.giver.id,
  35. nickname: flower.giver.nickname,
  36. avatar_url: flower.giver.avatar_url
  37. },
  38. recipient: {
  39. id: flower.recipient.id,
  40. nickname: flower.recipient.nickname,
  41. avatar_url: flower.recipient.avatar_url
  42. },
  43. comment: flower.comment,
  44. flower_window: "领读人可在当天或后一天发放小红花",
  45. created_at: flower.created_at
  46. }, status: :created
  47. else
  48. render json: { error: flower.errors.full_messages }, status: :unprocessable_entity
  49. end
  50. end
  51. # GET /api/reading_schedules/:reading_schedule_id/flowers
  52. def index
  53. reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
  54. flowers = reading_schedule.flowers.includes(:giver, :recipient, :check_in)
  55. render json: flowers.map { |flower|
  56. {
  57. id: flower.id,
  58. giver: {
  59. id: flower.giver.id,
  60. nickname: flower.giver.nickname,
  61. avatar_url: flower.giver.avatar_url
  62. },
  63. recipient: {
  64. id: flower.recipient.id,
  65. nickname: flower.recipient.nickname,
  66. avatar_url: flower.recipient.avatar_url
  67. },
  68. check_in: {
  69. id: flower.check_in.id,
  70. content: flower.check_in.content.truncate(100)
  71. },
  72. comment: flower.comment,
  73. created_at: flower.created_at
  74. }
  75. }
  76. end
  77. # GET /api/users/:user_id/flowers
  78. def user_flowers
  79. user = User.find(params[:user_id])
  80. flowers = Flower.where(recipient: user).includes(:giver, :reading_schedule, :check_in)
  81. render json: {
  82. total_count: flowers.count,
  83. flowers: flowers.map { |flower|
  84. {
  85. id: flower.id,
  86. giver: {
  87. id: flower.giver.id,
  88. nickname: flower.giver.nickname,
  89. avatar_url: flower.giver.avatar_url
  90. },
  91. reading_schedule: {
  92. id: flower.reading_schedule.id,
  93. day_number: flower.reading_schedule.day_number,
  94. date: flower.reading_schedule.date
  95. },
  96. check_in: {
  97. id: flower.check_in.id,
  98. content: flower.check_in.content.truncate(100)
  99. },
  100. comment: flower.comment,
  101. created_at: flower.created_at
  102. }
  103. }
  104. }
  105. end
  106. private
  107. def flower_params
  108. params.require(:flower).permit(:comment)
  109. end
  110. end

app/controllers/api/likes_controller.rb

0.0% lines covered

46 relevant lines. 0 lines covered and 46 lines missed.
    
  1. module Api
  2. class LikesController < Api::ApplicationController
  3. before_action :authenticate_user!
  4. # POST /api/posts/:post_id/like
  5. def create
  6. target = find_target
  7. if target.nil?
  8. return render json: { error: '目标不存在' }, status: :not_found
  9. end
  10. if Like.like!(current_user, target)
  11. render json: {
  12. message: '点赞成功',
  13. liked: true,
  14. likes_count: target.likes_count
  15. }
  16. else
  17. render json: { error: '已经点赞过了' }, status: :unprocessable_entity
  18. end
  19. end
  20. # DELETE /api/posts/:post_id/like
  21. def destroy
  22. target = find_target
  23. if target.nil?
  24. return render json: { error: '目标不存在' }, status: :not_found
  25. end
  26. if Like.unlike!(current_user, target)
  27. render json: {
  28. message: '取消点赞成功',
  29. liked: false,
  30. likes_count: target.likes_count
  31. }
  32. else
  33. render json: { error: '还未点赞' }, status: :unprocessable_entity
  34. end
  35. end
  36. private
  37. def find_target
  38. case params[:post_id]
  39. when nil
  40. nil
  41. else
  42. Post.find(params[:post_id])
  43. end
  44. rescue ActiveRecord::RecordNotFound
  45. nil
  46. end
  47. end
  48. end

app/controllers/api/optimized_posts_controller.rb

0.0% lines covered

130 relevant lines. 0 lines covered and 130 lines missed.
    
  1. # frozen_string_literal: true
  2. module Api
  3. # 优化版本的PostsController - 解决N+1查询问题
  4. class OptimizedPostsController < Api::ApplicationController
  5. before_action :authenticate_user!
  6. include AdminAuthorizable
  7. # GET /api/posts
  8. def index
  9. # 基础查询,预加载所有必要的关联
  10. @posts = base_posts_query
  11. # 按分类筛选
  12. if params[:category].present?
  13. @posts = @posts.by_category(params[:category])
  14. end
  15. # 分页处理
  16. @posts = paginate_posts(@posts)
  17. # 批量预加载权限信息
  18. preload_permissions(@posts, current_user) if current_user
  19. # 批量预加载点赞状态
  20. preload_like_status(@posts, current_user) if current_user
  21. render json: optimized_posts_json(@posts)
  22. end
  23. # GET /api/posts/:id
  24. def show
  25. @post = Post.find(params[:id])
  26. # 检查权限:普通用户看不到隐藏帖子
  27. unless current_user.any_admin?
  28. if @post.hidden?
  29. return render json: { error: "帖子已被隐藏" }, status: :not_found
  30. end
  31. end
  32. render json: optimized_post_json(@post)
  33. end
  34. private
  35. # 基础帖子查询
  36. def base_posts_query
  37. # 如果是管理员,可以看到所有帖子
  38. if current_user.any_admin?
  39. Post.includes(:user)
  40. .order(pinned: :desc, created_at: :desc)
  41. else
  42. Post.visible.includes(:user)
  43. .order(pinned: :desc, created_at: :desc)
  44. end
  45. end
  46. # 分页处理
  47. def paginate_posts(query)
  48. page = params[:page].to_i > 0 ? params[:page].to_i : 1
  49. per_page = params[:per_page].to_i > 0 ? [params[:per_page].to_i, 50].min : 20
  50. @total_count = query.count
  51. query.limit(per_page).offset((page - 1) * per_page)
  52. end
  53. # 批量预加载权限信息
  54. def preload_permissions(posts, user)
  55. post_ids = posts.map(&:id)
  56. # 批量获取权限信息
  57. permissions = PostPermissionService.batch_check_posts_permissions(
  58. post_ids,
  59. user.id,
  60. [:edit, :delete, :pin, :hide, :comment]
  61. )
  62. # 将权限信息附加到每个post对象
  63. posts.each do |post|
  64. post_id = post.id
  65. post.instance_variable_set(:@permissions, {
  66. can_edit: permissions.dig(:edit, post_id) || false,
  67. can_delete: permissions.dig(:delete, post_id) || false,
  68. can_pin: permissions.dig(:pin, post_id) || false,
  69. can_hide: permissions.dig(:hide, post_id) || false,
  70. can_comment: permissions.dig(:comment, post_id) || false
  71. })
  72. end
  73. end
  74. # 批量预加载点赞状态
  75. def preload_like_status(posts, user)
  76. post_ids = posts.map(&:id)
  77. # 一次性查询用户对所有帖子的点赞状态
  78. liked_post_ids = Like.where(
  79. user_id: user.id,
  80. target_type: 'Post',
  81. target_id: post_ids
  82. ).pluck(:target_id)
  83. # 将点赞状态附加到每个post对象
  84. posts.each do |post|
  85. post.instance_variable_set(:@current_user_liked, liked_post_ids.include?(post.id))
  86. end
  87. end
  88. # 优化的帖子JSON序列化
  89. def optimized_posts_json(posts)
  90. {
  91. posts: posts.map { |post| optimized_post_json(post, lite: true) },
  92. pagination: {
  93. current_page: params[:page].to_i > 0 ? params[:page].to_i : 1,
  94. per_page: params[:per_page].to_i > 0 ? [params[:per_page].to_i, 50].min : 20,
  95. total_count: @total_count,
  96. total_pages: (@total_count.to_f / [params[:per_page].to_i, 50].min).ceil,
  97. has_next: (params[:page].to_i > 0 ? params[:page].to_i : 1) * [params[:per_page].to_i, 50].min < @total_count,
  98. has_prev: (params[:page].to_i > 0 ? params[:page].to_i : 1) > 1
  99. }
  100. }
  101. end
  102. # 优化的单个帖子JSON序列化
  103. def optimized_post_json(post, lite: false)
  104. permissions = post.instance_variable_get(:@permissions) || {}
  105. liked_status = post.instance_variable_get(:@current_user_liked)
  106. result = {
  107. id: post.id,
  108. title: post.title,
  109. content: post.content,
  110. category: post.category,
  111. category_name: post.category_name,
  112. pinned: post.pinned,
  113. hidden: post.hidden,
  114. created_at: post.created_at,
  115. updated_at: post.updated_at,
  116. time_ago: post.time_ago_in_words(post.created_at),
  117. stats: {
  118. likes_count: post.likes_count,
  119. comments_count: post.comments_count
  120. },
  121. author: post.user.as_json_for_api
  122. }
  123. # 添加当前用户的交互状态(仅在需要时)
  124. if current_user && !lite
  125. result[:interactions] = {
  126. liked: liked_status || post.liked_by?(current_user),
  127. can_edit: permissions[:can_edit] || post.can_edit?(current_user),
  128. can_delete: permissions[:can_delete] || post.can_delete?(current_user),
  129. can_pin: permissions[:can_pin] || post.can_pin?(current_user),
  130. can_hide: permissions[:can_hide] || post.can_hide?(current_user),
  131. can_comment: permissions[:can_comment] || post.can_comment?(current_user)
  132. }
  133. end
  134. # 包含关联数据(仅在详情页面)
  135. if !lite && params[:include_comments] == 'true'
  136. result[:recent_comments] = post.comments.limit(5).includes(:user).map(&:as_json_for_api)
  137. end
  138. if !lite && params[:include_likes] == 'true'
  139. result[:recent_likes] = post.likes.limit(10).includes(:user).map do |like|
  140. {
  141. id: like.id,
  142. user: like.user.as_json_for_api,
  143. created_at: like.created_at
  144. }
  145. end
  146. end
  147. result
  148. end
  149. def post_params
  150. params.require(:post).permit(:title, :content, :category, :images, tags: [])
  151. end
  152. end
  153. end

app/controllers/api/posts_controller.rb

0.0% lines covered

142 relevant lines. 0 lines covered and 142 lines missed.
    
  1. module Api
  2. class PostsController < Api::ApplicationController
  3. before_action :authenticate_user!
  4. include AdminAuthorizable
  5. # GET /api/posts
  6. def index
  7. @posts = Post.visible.includes(:user).pinned_first
  8. # 如果是管理员,可以看到所有帖子
  9. if current_user.any_admin?
  10. @posts = Post.includes(:user).pinned_first
  11. end
  12. # 按分类筛选
  13. if params[:category].present?
  14. @posts = @posts.by_category(params[:category])
  15. end
  16. render json: @posts.map { |post|
  17. post.instance_variable_set(:@can_edit_current_user, post.can_edit?(current_user))
  18. post.instance_variable_set(:@current_user, current_user)
  19. post.as_json
  20. }
  21. end
  22. # GET /api/posts/:id
  23. def show
  24. @post = Post.find(params[:id])
  25. # 检查权限:普通用户看不到隐藏帖子
  26. unless current_user.any_admin?
  27. if @post.hidden?
  28. return render json: { error: "帖子已被隐藏" }, status: :not_found
  29. end
  30. end
  31. @post.instance_variable_set(:@can_edit_current_user, @post.can_edit?(current_user))
  32. @post.instance_variable_set(:@current_user, current_user)
  33. render json: @post.as_json
  34. end
  35. # POST /api/posts
  36. def create
  37. # 使用PostManagementService处理帖子创建
  38. service_result = PostManagementService.create_post!(current_user, post_params)
  39. if service_result.success?
  40. render json: service_result.result, status: :created
  41. else
  42. render json: { errors: service_result.error_messages }, status: :unprocessable_entity
  43. end
  44. end
  45. # PUT /api/posts/:id
  46. def update
  47. @post = Post.find(params[:id])
  48. # 使用PostManagementService处理帖子更新
  49. service_result = PostManagementService.update_post!(@post, current_user, post_params)
  50. if service_result.success?
  51. render json: service_result.result
  52. else
  53. error_message = service_result.first_error
  54. status_code = error_message.include?("权限") ? :forbidden : :unprocessable_entity
  55. # 对于验证错误,使用errors格式;对于权限错误,使用error格式
  56. if service_result.error_messages.any? && service_result.error_messages.first.include?("can't be blank")
  57. render json: { errors: service_result.error_messages }, status: status_code
  58. else
  59. render json: { error: error_message }, status: status_code
  60. end
  61. end
  62. end
  63. # DELETE /api/posts/:id
  64. def destroy
  65. @post = Post.find(params[:id])
  66. # 使用PostManagementService处理帖子删除
  67. service_result = PostManagementService.delete_post!(@post, current_user)
  68. if service_result.success?
  69. head :no_content
  70. else
  71. error_message = service_result.first_error
  72. status_code = error_message.include?("权限") ? :forbidden : :unprocessable_entity
  73. render json: { error: error_message }, status: status_code
  74. end
  75. end
  76. # POST /api/posts/:id/pin # 置顶帖子
  77. def pin
  78. authenticate_admin! and return
  79. @post = Post.find(params[:id])
  80. # 使用PostManagementService处理帖子置顶
  81. service_result = PostManagementService.pin_post!(@post, current_user)
  82. if service_result.success?
  83. render json: service_result.result
  84. else
  85. render json: { error: service_result.first_error }, status: :forbidden
  86. end
  87. end
  88. # POST /api/posts/:id/unpin # 取消置顶
  89. def unpin
  90. authenticate_admin! and return
  91. @post = Post.find(params[:id])
  92. unless @post.can_pin?(current_user)
  93. render json: { error: "无权限取消置顶此帖子" }, status: :forbidden
  94. return
  95. end
  96. @post.unpin!
  97. render json: {
  98. message: "帖子已取消置顶",
  99. post: @post.as_json
  100. }
  101. end
  102. # POST /api/posts/:id/hide # 隐藏帖子
  103. def hide
  104. authenticate_admin! and return
  105. @post = Post.find(params[:id])
  106. unless @post.can_hide?(current_user)
  107. render json: { error: "无权限隐藏此帖子" }, status: :forbidden
  108. return
  109. end
  110. @post.hide!
  111. render json: {
  112. message: "帖子已隐藏",
  113. post: @post.as_json
  114. }
  115. end
  116. # POST /api/posts/:id/unhide # 显示帖子
  117. def unhide
  118. authenticate_admin! and return
  119. @post = Post.find(params[:id])
  120. unless @post.can_hide?(current_user)
  121. render json: { error: "无权限显示此帖子" }, status: :forbidden
  122. return
  123. end
  124. @post.unhide!
  125. render json: {
  126. message: "帖子已显示",
  127. post: @post.as_json
  128. }
  129. end
  130. # POST /api/posts/:id/like # 点赞帖子
  131. def like
  132. @post = Post.find(params[:id])
  133. if Like.like!(current_user, @post)
  134. render json: {
  135. message: "点赞成功",
  136. liked: true,
  137. likes_count: @post.likes_count
  138. }
  139. else
  140. render json: { error: "已经点赞过了" }, status: :unprocessable_entity
  141. end
  142. end
  143. # DELETE /api/posts/:id/like # 取消点赞
  144. def unlike
  145. @post = Post.find(params[:id])
  146. if Like.unlike!(current_user, @post)
  147. render json: {
  148. message: "取消点赞成功",
  149. liked: false,
  150. likes_count: @post.likes_count
  151. }
  152. else
  153. render json: { error: "还未点赞" }, status: :unprocessable_entity
  154. end
  155. end
  156. private
  157. def post_params
  158. params.require(:post).permit(:title, :content, :category, :images, tags: [])
  159. end
  160. end
  161. end

app/controllers/api/uploads_controller.rb

0.0% lines covered

36 relevant lines. 0 lines covered and 36 lines missed.
    
  1. module Api
  2. class UploadsController < Api::ApplicationController
  3. before_action :authenticate_user!
  4. # POST /api/upload/image
  5. def create
  6. return render json: { error: '请选择图片文件' }, status: :bad_request unless params[:file]
  7. uploaded_file = params[:file]
  8. # 验证文件类型
  9. unless uploaded_file.content_type.in?(['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])
  10. return render json: { error: '只支持 JPG、PNG、GIF 格式的图片' }, status: :bad_request
  11. end
  12. # 验证文件大小(最大5MB)
  13. if uploaded_file.size > 5.megabytes
  14. return render json: { error: '图片大小不能超过5MB' }, status: :bad_request
  15. end
  16. begin
  17. # 生成唯一文件名
  18. file_name = "#{SecureRandom.uuid}_#{uploaded_file.original_filename}"
  19. # 这里应该将文件存储到云存储服务,如阿里云OSS、腾讯云COS等
  20. # 暂时存储到本地,生产环境需要使用云存储
  21. file_path = Rails.root.join('tmp', 'uploads', file_name)
  22. FileUtils.mkdir_p(File.dirname(file_path))
  23. File.open(file_path, 'wb') do |file|
  24. file.write(uploaded_file.read)
  25. end
  26. # 生成访问URL(开发环境使用本地路径)
  27. url = "/uploads/#{file_name}"
  28. render json: {
  29. message: '图片上传成功',
  30. url: url,
  31. file_name: file_name
  32. }
  33. rescue => e
  34. Rails.logger.error "图片上传失败: #{e.message}"
  35. render json: { error: '图片上传失败,请重试' }, status: :internal_server_error
  36. end
  37. end
  38. private
  39. def authenticate_user!
  40. return head :unauthorized unless current_user
  41. end
  42. end
  43. end

app/controllers/api/v1/analytics_controller.rb

0.0% lines covered

378 relevant lines. 0 lines covered and 378 lines missed.
    
  1. # frozen_string_literal: true
  2. class Api::V1::AnalyticsController < ApplicationController
  3. before_action :authenticate_user!
  4. before_action :require_admin_for_system_analytics
  5. # GET /api/v1/analytics/overview
  6. # 获取系统总览统计(管理员)
  7. def overview
  8. render json: {
  9. success: true,
  10. data: AnalyticsService.system_overview,
  11. generated_at: Time.current
  12. }
  13. end
  14. # GET /api/v1/analytics/dashboard
  15. # 获取用户仪表板数据
  16. def dashboard
  17. days = params[:days]&.to_i || 30
  18. user_analytics = AnalyticsService.user_analytics(current_user, days)
  19. render json: {
  20. success: true,
  21. data: user_analytics,
  22. period: "#{days} 天",
  23. generated_at: Time.current
  24. }
  25. end
  26. # GET /api/v1/analytics/events/:id
  27. # 获取活动详细统计
  28. def event_stats
  29. event = ReadingEvent.find(params[:id])
  30. # 检查权限:活动创建者、管理员或参与者可以查看统计
  31. unless can_view_event_analytics?(event)
  32. return render json: {
  33. success: false,
  34. error: '无权限查看此活动统计'
  35. }, status: :forbidden
  36. end
  37. analytics = AnalyticsService.event_analytics(event)
  38. render json: {
  39. success: true,
  40. data: analytics,
  41. generated_at: Time.current
  42. }
  43. end
  44. # GET /api/v1/analytics/trends
  45. # 获取趋势数据
  46. def trends
  47. metric = params[:metric]&.to_sym
  48. period = params[:period]&.to_sym || :week
  49. days = params[:days]&.to_i || 30
  50. unless [:check_ins, :flowers, :users, :events, :notifications].include?(metric)
  51. return render json: {
  52. success: false,
  53. error: '无效的指标类型。支持的类型: check_ins, flowers, users, events, notifications'
  54. }, status: :bad_request
  55. end
  56. unless [:day, :week, :month].include?(period)
  57. return render json: {
  58. success: false,
  59. error: '无效的时间周期。支持的周期: day, week, month'
  60. }, status: :bad_request
  61. end
  62. trend_data = AnalyticsService.trend_data(metric, period, days)
  63. render json: {
  64. success: true,
  65. metric: metric,
  66. period: period,
  67. days: days,
  68. data: trend_data,
  69. generated_at: Time.current
  70. }
  71. end
  72. # GET /api/v1/analytics/leaderboards
  73. # 获取排行榜数据
  74. def leaderboards
  75. type = params[:type]&.to_sym || :flowers
  76. limit = params[:limit]&.to_i || 10
  77. period = params[:period]&.to_sym || :all_time
  78. unless [:flowers, :check_ins, :participation].include?(type)
  79. return render json: {
  80. success: false,
  81. error: '无效的排行榜类型。支持的类型: flowers, check_ins, participation'
  82. }, status: :bad_request
  83. end
  84. unless [:today, :week, :month, :all_time].include?(period)
  85. return render json: {
  86. success: false,
  87. error: '无效的时间周期。支持的周期: today, week, month, all_time'
  88. }, status: :bad_request
  89. end
  90. leaderboard_data = AnalyticsService.leaderboards(type, limit, period)
  91. render json: {
  92. success: true,
  93. type: type,
  94. period: period,
  95. limit: limit,
  96. data: leaderboard_data,
  97. generated_at: Time.current
  98. }
  99. end
  100. # GET /api/v1/analytics/users/:id
  101. # 获取用户详细统计(管理员功能)
  102. def user_stats
  103. user = User.find(params[:id])
  104. days = params[:days]&.to_i || 30
  105. unless current_user.any_admin? || current_user.id == user.id
  106. return render json: {
  107. success: false,
  108. error: '无权限查看此用户统计'
  109. }, status: :forbidden
  110. end
  111. analytics = AnalyticsService.user_analytics(user, days)
  112. render json: {
  113. success: true,
  114. data: analytics,
  115. period: "#{days} 天",
  116. generated_at: Time.current
  117. }
  118. end
  119. # GET /api/v1/analytics/summary
  120. # 获取简化的统计数据摘要
  121. def summary
  122. summary_data = {
  123. system: {
  124. total_users: User.count,
  125. active_events: ReadingEvent.where(status: ['enrolling', 'in_progress']).count,
  126. today_check_ins: CheckIn.where('created_at >= ?', Date.current).count,
  127. today_flowers: Flower.where('created_at >= ?', Date.current).count
  128. },
  129. user: {
  130. enrolled_events: current_user.event_enrollments.where(status: 'enrolled').count,
  131. my_check_ins: current_user.check_ins.where('created_at >= ?', 7.days.ago).count,
  132. flowers_received: current_user.received_flowers.where('created_at >= ?', 7.days.ago).count,
  133. notifications_unread: current_user.received_notifications.unread.count
  134. }
  135. }
  136. render json: {
  137. success: true,
  138. data: summary_data,
  139. generated_at: Time.current
  140. }
  141. end
  142. # GET /api/v1/analytics/reports
  143. # 生成报告(管理员功能)
  144. def reports
  145. report_type = params[:type]&.to_sym
  146. format = params[:format]&.to_sym || :json
  147. unless current_user.any_admin?
  148. return render json: {
  149. success: false,
  150. error: '需要管理员权限'
  151. }, status: :forbidden
  152. end
  153. case report_type
  154. when :monthly
  155. report = generate_monthly_report
  156. when :activity
  157. report = generate_activity_report
  158. when :engagement
  159. report = generate_engagement_report
  160. else
  161. return render json: {
  162. success: false,
  163. error: '无效的报告类型。支持的类型: monthly, activity, engagement'
  164. }, status: :bad_request
  165. end
  166. render json: {
  167. success: true,
  168. report_type: report_type,
  169. data: report,
  170. generated_at: Time.current
  171. }
  172. end
  173. # GET /api/v1/analytics/export
  174. # 导出数据(管理员功能)
  175. def export
  176. export_type = params[:type]&.to_sym
  177. unless current_user.any_admin?
  178. return render json: {
  179. success: false,
  180. error: '需要管理员权限'
  181. }, status: :forbidden
  182. end
  183. case export_type
  184. when :users
  185. data = export_users_data
  186. when :events
  187. data = export_events_data
  188. when :flowers
  189. data = export_flowers_data
  190. else
  191. return render json: {
  192. success: false,
  193. error: '无效的导出类型。支持的类型: users, events, flowers'
  194. }, status: :bad_request
  195. end
  196. render json: {
  197. success: true,
  198. export_type: export_type,
  199. data: data,
  200. record_count: data.count,
  201. generated_at: Time.current
  202. }
  203. end
  204. private
  205. # 检查是否为系统分析需要管理员权限
  206. def require_admin_for_system_analytics
  207. return unless [:overview, :reports, :export].include?(action_name.to_sym)
  208. unless current_user.any_admin?
  209. render json: {
  210. success: false,
  211. error: '需要管理员权限'
  212. }, status: :forbidden
  213. end
  214. end
  215. # 检查是否可以查看活动分析
  216. def can_view_event_analytics?(event)
  217. return true if current_user.any_admin?
  218. return true if event.leader_id == current_user.id
  219. return true if event.participants.include?(current_user)
  220. false
  221. end
  222. # 生成月度报告
  223. def generate_monthly_report
  224. current_month = Date.current.beginning_of_month
  225. last_month = current_month - 1.month
  226. {
  227. current_month: {
  228. period: current_month.strftime('%Y年%m月'),
  229. users: {
  230. new: User.where(created_at: current_month..(current_month + 1.month)).count,
  231. active: active_users_in_period(current_month, (current_month + 1.month))
  232. },
  233. events: {
  234. created: ReadingEvent.where(created_at: current_month..(current_month + 1.month)).count,
  235. completed: ReadingEvent.where(status: 'completed', updated_at: current_month..(current_month + 1.month)).count
  236. },
  237. engagement: {
  238. check_ins: CheckIn.where(created_at: current_month..(current_month + 1.month)).count,
  239. flowers: Flower.where(created_at: current_month..(current_month + 1.month)).count
  240. }
  241. },
  242. comparison: {
  243. period: last_month.strftime('%Y年%m月'),
  244. user_growth: calculate_growth_rate(
  245. User.where(created_at: last_month..current_month).count,
  246. User.where(created_at: (last_month - 1.month)..last_month).count
  247. ),
  248. engagement_growth: calculate_growth_rate(
  249. CheckIn.where(created_at: current_month..(current_month + 1.month)).count,
  250. CheckIn.where(created_at: last_month..current_month).count
  251. )
  252. }
  253. }
  254. end
  255. # 生成活动报告
  256. def generate_activity_report
  257. status = params[:status] || 'all'
  258. events = ReadingEvent.all
  259. events = events.where(status: status) if status != 'all'
  260. events.map do |event|
  261. {
  262. id: event.id,
  263. title: event.title,
  264. status: event.status,
  265. participants_count: event.event_enrollments.where(status: 'enrolled').count,
  266. check_ins_count: event.check_ins.count,
  267. flowers_count: event.flowers_count,
  268. completion_rate: AnalyticsService.send(:calculate_event_completion_rate, event.event_enrollments),
  269. engagement_score: AnalyticsService.send(:calculate_event_engagement_score, event)
  270. }
  271. end
  272. end
  273. # 生成参与度报告
  274. def generate_engagement_report
  275. {
  276. user_engagement: user_engagement_metrics,
  277. event_engagement: event_engagement_metrics,
  278. daily_activity: daily_activity_metrics,
  279. trends: engagement_trends
  280. }
  281. end
  282. # 导出用户数据
  283. def export_users_data
  284. User.all.map do |user|
  285. {
  286. id: user.id,
  287. nickname: user.nickname,
  288. wx_openid: user.wx_openid,
  289. role: user.role_as_string,
  290. created_at: user.created_at,
  291. last_activity: user.check_ins.maximum(:created_at) || user.created_at,
  292. events_count: user.event_enrollments.count,
  293. check_ins_count: user.check_ins.count,
  294. flowers_given: user.given_flowers.count,
  295. flowers_received: user.received_flowers.count
  296. }
  297. end
  298. end
  299. # 导出活动数据
  300. def export_events_data
  301. ReadingEvent.all.map do |event|
  302. {
  303. id: event.id,
  304. title: event.title,
  305. book_name: event.book_name,
  306. leader: event.leader&.nickname,
  307. status: event.status,
  308. approval_status: event.approval_status,
  309. start_date: event.start_date,
  310. end_date: event.end_date,
  311. max_participants: event.max_participants,
  312. enrolled_count: event.event_enrollments.where(status: 'enrolled').count,
  313. check_ins_count: event.check_ins.count,
  314. flowers_count: event.flowers_count,
  315. created_at: event.created_at
  316. }
  317. end
  318. end
  319. # 导出小红花数据
  320. def export_flowers_data
  321. Flower.includes(:giver, :recipient, :check_in).all.map do |flower|
  322. {
  323. id: flower.id,
  324. giver: flower.giver&.nickname,
  325. recipient: flower.recipient&.nickname,
  326. check_in_content: flower.check_in&.content&.truncate(50),
  327. amount: flower.amount,
  328. flower_type: flower.flower_type,
  329. comment: flower.comment,
  330. created_at: flower.created_at
  331. }
  332. end
  333. end
  334. # 辅助方法
  335. def active_users_in_period(start_time, end_time)
  336. User.joins(:check_ins)
  337. .where('check_ins.created_at >= ? AND check_ins.created_at < ?', start_time, end_time)
  338. .distinct
  339. .count
  340. end
  341. def calculate_growth_rate(current, previous)
  342. return 0 if previous == 0
  343. ((current - previous).to_f / previous * 100).round(2)
  344. end
  345. def user_engagement_metrics
  346. {
  347. average_check_ins_per_user: CheckIn.count.to_f / [User.count, 1].max,
  348. average_flowers_per_user: Flower.count.to_f / [User.count, 1].max,
  349. user_retention_rate: AnalyticsService.send(:user_retention_rate)
  350. }
  351. end
  352. def event_engagement_metrics
  353. {
  354. average_participation_rate: calculate_average_participation_rate,
  355. average_completion_rate: calculate_average_completion_rate,
  356. most_active_events: most_active_events(5)
  357. }
  358. end
  359. def daily_activity_metrics
  360. (7.days.ago.to_date..Date.current).map do |date|
  361. {
  362. date: date.strftime('%Y-%m-%d'),
  363. check_ins: CheckIn.where(created_at: date.beginning_of_day..date.end_of_day).count,
  364. flowers: Flower.where(created_at: date.beginning_of_day..date.end_of_day).count,
  365. active_users: active_users_in_period(date.beginning_of_day, date.end_of_day)
  366. }
  367. end
  368. end
  369. def engagement_trends
  370. {
  371. check_ins_trend: AnalyticsService.trend_data(:check_ins, :week, 30),
  372. flowers_trend: AnalyticsService.trend_data(:flowers, :week, 30),
  373. users_trend: AnalyticsService.trend_data(:users, :week, 30)
  374. }
  375. end
  376. def calculate_average_participation_rate
  377. total_events = ReadingEvent.where(status: ['enrolling', 'in_progress', 'completed']).count
  378. return 0 if total_events == 0
  379. total_capacity = ReadingEvent.where(status: ['enrolling', 'in_progress', 'completed'])
  380. .sum(:max_participants)
  381. total_enrolled = ReadingEvent.joins(:event_enrollments)
  382. .where(event_enrollments: { status: 'enrolled' })
  383. .count
  384. (total_enrolled.to_f / total_capacity * 100).round(2)
  385. end
  386. def calculate_average_completion_rate
  387. completed_enrollments = EventEnrollment.where(status: 'completed').count
  388. total_enrollments = EventEnrollment.where.not(status: 'cancelled').count
  389. return 0 if total_enrollments == 0
  390. (completed_enrollments.to_f / total_enrollments * 100).round(2)
  391. end
  392. def most_active_events(limit = 5)
  393. ReadingEvent.joins(:check_ins)
  394. .group('reading_events.id')
  395. .select('reading_events.*, COUNT(check_ins.id) as check_ins_count')
  396. .order('check_ins_count DESC')
  397. .limit(limit)
  398. .map do |event|
  399. {
  400. id: event.id,
  401. title: event.title,
  402. check_ins_count: event.check_ins_count,
  403. flowers_count: event.flowers_count
  404. }
  405. end
  406. end
  407. end

app/controllers/api/v1/approval_workflow_controller.rb

0.0% lines covered

498 relevant lines. 0 lines covered and 498 lines missed.
    
  1. class Api::V1::ApprovalWorkflowController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :check_admin_permissions
  4. # POST /api/v1/approval_workflow/submit_for_approval
  5. # 提交活动审批
  6. def submit_for_approval
  7. event_id = params[:event_id]
  8. workflow_type = params[:workflow_type]&.to_sym || :standard
  9. unless event_id.present?
  10. render_error(
  11. message: '请提供活动ID',
  12. code: 'EVENT_ID_REQUIRED',
  13. status: :unprocessable_entity
  14. )
  15. return
  16. end
  17. event = ReadingEvent.find_by(id: event_id)
  18. unless event
  19. render_error(
  20. message: '活动不存在',
  21. code: 'EVENT_NOT_FOUND',
  22. status: :not_found
  23. )
  24. return
  25. end
  26. # 检查权限(只有活动创建者可以提交审批)
  27. unless event.leader == current_user
  28. render_error(
  29. message: '只有活动创建者可以提交审批',
  30. code: 'FORBIDDEN',
  31. status: :forbidden
  32. )
  33. return
  34. end
  35. service = ActivityApprovalWorkflowService.submit_for_approval!(event, workflow_type: workflow_type)
  36. if service.success?
  37. render_success(
  38. data: service.result,
  39. message: service.result[:message]
  40. )
  41. log_api_call('approval_workflow#submit_for_approval')
  42. else
  43. render_error(
  44. message: service.error_message,
  45. code: 'SUBMIT_FOR_APPROVAL_FAILED',
  46. status: :unprocessable_entity
  47. )
  48. end
  49. rescue ActiveRecord::RecordNotFound
  50. render_error(
  51. message: '活动不存在',
  52. code: 'EVENT_NOT_FOUND',
  53. status: :not_found
  54. )
  55. rescue => e
  56. render_error(
  57. message: '提交审批失败',
  58. errors: [e.message],
  59. code: 'SUBMIT_FOR_APPROVAL_ERROR'
  60. )
  61. end
  62. # POST /api/v1/approval_workflow/approve_event
  63. # 审批通过活动
  64. def approve_event
  65. event_id = params[:event_id]
  66. reason = params[:reason]
  67. notes = params[:notes]
  68. unless event_id.present?
  69. render_error(
  70. message: '请提供活动ID',
  71. code: 'EVENT_ID_REQUIRED',
  72. status: :unprocessable_entity
  73. )
  74. return
  75. end
  76. event = ReadingEvent.find_by(id: event_id)
  77. unless event
  78. render_error(
  79. message: '活动不存在',
  80. code: 'EVENT_NOT_FOUND',
  81. status: :not_found
  82. )
  83. return
  84. end
  85. service = ActivityApprovalWorkflowService.approve!(event, current_user, reason: reason, notes: notes)
  86. if service.success?
  87. render_success(
  88. data: service.result,
  89. message: service.result[:message]
  90. )
  91. log_api_call('approval_workflow#approve_event')
  92. else
  93. render_error(
  94. message: service.error_message,
  95. code: 'APPROVE_EVENT_FAILED',
  96. status: :unprocessable_entity
  97. )
  98. end
  99. rescue ActiveRecord::RecordNotFound
  100. render_error(
  101. message: '活动不存在',
  102. code: 'EVENT_NOT_FOUND',
  103. status: :not_found
  104. )
  105. rescue => e
  106. render_error(
  107. message: '审批通过失败',
  108. errors: [e.message],
  109. code: 'APPROVE_EVENT_ERROR'
  110. )
  111. end
  112. # POST /api/v1/approval_workflow/reject_event
  113. # 审批拒绝活动
  114. def reject_event
  115. event_id = params[:event_id]
  116. reason = params[:reason]
  117. notes = params[:notes]
  118. unless event_id.present?
  119. render_error(
  120. message: '请提供活动ID',
  121. code: 'EVENT_ID_REQUIRED',
  122. status: :unprocessable_entity
  123. )
  124. return
  125. end
  126. unless reason.present?
  127. render_error(
  128. message: '请提供拒绝理由',
  129. code: 'REJECTION_REASON_REQUIRED',
  130. status: :unprocessable_entity
  131. )
  132. return
  133. end
  134. event = ReadingEvent.find_by(id: event_id)
  135. unless event
  136. render_error(
  137. message: '活动不存在',
  138. code: 'EVENT_NOT_FOUND',
  139. status: :not_found
  140. )
  141. return
  142. end
  143. service = ActivityApprovalWorkflowService.reject!(event, current_user, reason, notes: notes)
  144. if service.success?
  145. render_success(
  146. data: service.result,
  147. message: service.result[:message]
  148. )
  149. log_api_call('approval_workflow#reject_event')
  150. else
  151. render_error(
  152. message: service.error_message,
  153. code: 'REJECT_EVENT_FAILED',
  154. status: :unprocessable_entity
  155. )
  156. end
  157. rescue ActiveRecord::RecordNotFound
  158. render_error(
  159. message: '活动不存在',
  160. code: 'EVENT_NOT_FOUND',
  161. status: :not_found
  162. )
  163. rescue => e
  164. render_error(
  165. message: '审批拒绝失败',
  166. errors: [e.message],
  167. code: 'REJECT_EVENT_ERROR'
  168. )
  169. end
  170. # POST /api/v1/approval_workflow/batch_approve
  171. # 批量审批通过
  172. def batch_approve
  173. event_ids = params[:event_ids]
  174. reason = params[:reason]
  175. unless event_ids.present? && event_ids.is_a?(Array)
  176. render_error(
  177. message: '请提供有效的活动ID列表',
  178. code: 'EVENT_IDS_REQUIRED',
  179. status: :unprocessable_entity
  180. )
  181. return
  182. end
  183. service = ActivityApprovalWorkflowService.batch_approve!(event_ids, current_user, reason: reason)
  184. if service.success?
  185. render_success(
  186. data: service.result,
  187. message: service.result[:message]
  188. )
  189. log_api_call('approval_workflow#batch_approve')
  190. else
  191. render_error(
  192. message: service.error_message,
  193. code: 'BATCH_APPROVE_FAILED',
  194. status: :unprocessable_entity
  195. )
  196. end
  197. rescue => e
  198. render_error(
  199. message: '批量审批失败',
  200. errors: [e.message],
  201. code: 'BATCH_APPROVE_ERROR'
  202. )
  203. end
  204. # POST /api/v1/approval_workflow/batch_reject
  205. # 批量审批拒绝
  206. def batch_reject
  207. event_ids = params[:event_ids]
  208. reason = params[:reason]
  209. notes = params[:notes]
  210. unless event_ids.present? && event_ids.is_a?(Array)
  211. render_error(
  212. message: '请提供有效的活动ID列表',
  213. code: 'EVENT_IDS_REQUIRED',
  214. status: :unprocessable_entity
  215. )
  216. return
  217. end
  218. unless reason.present?
  219. render_error(
  220. message: '请提供拒绝理由',
  221. code: 'REJECTION_REASON_REQUIRED',
  222. status: :unprocessable_entity
  223. )
  224. return
  225. end
  226. service = ActivityApprovalWorkflowService.batch_reject!(event_ids, current_user, reason, notes: notes)
  227. if service.success?
  228. render_success(
  229. data: service.result,
  230. message: service.result[:message]
  231. )
  232. log_api_call('approval_workflow#batch_reject')
  233. else
  234. render_error(
  235. message: service.error_message,
  236. code: 'BATCH_REJECT_FAILED',
  237. status: :unprocessable_entity
  238. )
  239. end
  240. rescue => e
  241. render_error(
  242. message: '批量拒绝失败',
  243. errors: [e.message],
  244. code: 'BATCH_REJECT_ERROR'
  245. )
  246. end
  247. # GET /api/v1/approval_workflow/approval_queue
  248. # 获取审批队列
  249. def approval_queue
  250. filters = {
  251. page: safe_integer_param(params[:page]) || 1,
  252. per_page: safe_integer_param(params[:per_page]) || 20,
  253. leader_id: safe_integer_param(params[:leader_id]),
  254. activity_mode: params[:activity_mode],
  255. fee_type: params[:fee_type],
  256. submitted_since: parse_date_param(params[:submitted_since]),
  257. submitted_until: parse_date_param(params[:submitted_until])
  258. }.compact
  259. service = ActivityApprovalWorkflowService.approval_queue(current_user, filters: filters)
  260. if service.success?
  261. render_success(
  262. data: service.result
  263. )
  264. log_api_call('approval_workflow#approval_queue')
  265. else
  266. render_error(
  267. message: service.error_message,
  268. code: 'GET_APPROVAL_QUEUE_FAILED',
  269. status: :unprocessable_entity
  270. )
  271. end
  272. rescue => e
  273. render_error(
  274. message: '获取审批队列失败',
  275. errors: [e.message],
  276. code: 'GET_APPROVAL_QUEUE_ERROR'
  277. )
  278. end
  279. # GET /api/v1/approval_workflow/approval_statistics
  280. # 获取审批统计
  281. def approval_statistics
  282. date_range = nil
  283. if params[:start_date].present? && params[:end_date].present?
  284. start_date = parse_date_param(params[:start_date])
  285. end_date = parse_date_param(params[:end_date])
  286. date_range = (start_date..end_date) if start_date && end_date
  287. end
  288. service = ActivityApprovalWorkflowService.approval_statistics(current_user, date_range: date_range)
  289. if service.success?
  290. render_success(
  291. data: service.result
  292. )
  293. log_api_call('approval_workflow#approval_statistics')
  294. else
  295. render_error(
  296. message: service.error_message,
  297. code: 'GET_APPROVAL_STATISTICS_FAILED',
  298. status: :unprocessable_entity
  299. )
  300. end
  301. rescue => e
  302. render_error(
  303. message: '获取审批统计失败',
  304. errors: [e.message],
  305. code: 'GET_APPROVAL_STATISTICS_ERROR'
  306. )
  307. end
  308. # POST /api/v1/approval_workflow/escalate_approval
  309. # 升级审批
  310. def escalate_approval
  311. event_id = params[:event_id]
  312. escalation_reason = params[:escalation_reason]
  313. unless event_id.present?
  314. render_error(
  315. message: '请提供活动ID',
  316. code: 'EVENT_ID_REQUIRED',
  317. status: :unprocessable_entity
  318. )
  319. return
  320. end
  321. unless escalation_reason.present?
  322. render_error(
  323. message: '请提供升级理由',
  324. code: 'ESCALATION_REASON_REQUIRED',
  325. status: :unprocessable_entity
  326. )
  327. return
  328. end
  329. event = ReadingEvent.find_by(id: event_id)
  330. unless event
  331. render_error(
  332. message: '活动不存在',
  333. code: 'EVENT_NOT_FOUND',
  334. status: :not_found
  335. )
  336. return
  337. end
  338. service = ActivityApprovalWorkflowService.escalate!(event, current_user, escalation_reason)
  339. if service.success?
  340. render_success(
  341. data: service.result,
  342. message: service.result[:message]
  343. )
  344. log_api_call('approval_workflow#escalate_approval')
  345. else
  346. render_error(
  347. message: service.error_message,
  348. code: 'ESCALATE_APPROVAL_FAILED',
  349. status: :unprocessable_entity
  350. )
  351. end
  352. rescue ActiveRecord::RecordNotFound
  353. render_error(
  354. message: '活动不存在',
  355. code: 'EVENT_NOT_FOUND',
  356. status: :not_found
  357. )
  358. rescue => e
  359. render_error(
  360. message: '升级审批失败',
  361. errors: [e.message],
  362. code: 'ESCALATE_APPROVAL_ERROR'
  363. )
  364. end
  365. # GET /api/v1/approval_workflow/event_approval_status
  366. # 获取活动审批状态
  367. def event_approval_status
  368. event_id = params[:event_id]
  369. unless event_id.present?
  370. render_error(
  371. message: '请提供活动ID',
  372. code: 'EVENT_ID_REQUIRED',
  373. status: :unprocessable_entity
  374. )
  375. return
  376. end
  377. event = ReadingEvent.find_by(id: event_id)
  378. unless event
  379. render_error(
  380. message: '活动不存在',
  381. code: 'EVENT_NOT_FOUND',
  382. status: :not_found
  383. )
  384. return
  385. end
  386. # 检查查看权限
  387. unless can_view_event_approval_status?(event)
  388. render_error(
  389. message: '权限不足,无法查看审批状态',
  390. code: 'FORBIDDEN',
  391. status: :forbidden
  392. )
  393. return
  394. end
  395. approval_status_data = {
  396. event_id: event.id,
  397. title: event.title,
  398. status: event.status,
  399. approval_status: event.approval_status,
  400. submitted_for_approval_at: event.submitted_for_approval_at,
  401. approved_at: event.approved_at,
  402. approver: event.approver ? user_info(event.approver) : nil,
  403. approval_reason: event.approval_reason,
  404. approval_notes: event.approval_notes,
  405. rejection_reason: event.rejection_reason,
  406. escalated_at: event.escalated_at,
  407. escalated_by: event.escalated_by ? user_info(event.escalated_by) : nil,
  408. escalation_reason: event.escalation_reason,
  409. can_submit_for_approval: event.can_submit_for_approval?,
  410. can_resubmit_for_approval: event.can_resubmit_for_approval?,
  411. approval_queue_position: get_approval_queue_position(event),
  412. validation_status: validate_event_for_approval_display(event)
  413. }
  414. render_success(
  415. data: approval_status_data
  416. )
  417. log_api_call('approval_workflow#event_approval_status')
  418. rescue ActiveRecord::RecordNotFound
  419. render_error(
  420. message: '活动不存在',
  421. code: 'EVENT_NOT_FOUND',
  422. status: :not_found
  423. )
  424. rescue => e
  425. render_error(
  426. message: '获取审批状态失败',
  427. errors: [e.message],
  428. code: 'GET_APPROVAL_STATUS_ERROR'
  429. )
  430. end
  431. private
  432. # 检查管理员权限
  433. def check_admin_permissions
  434. unless current_user.can_approve_events? || current_user.can_view_approval_queue?
  435. render_error(
  436. message: '权限不足',
  437. code: 'FORBIDDEN',
  438. status: :forbidden
  439. )
  440. end
  441. end
  442. # 检查是否可以查看活动审批状态
  443. def can_view_event_approval_status?(event)
  444. # 活动创建者可以查看
  445. return true if event.leader == current_user
  446. # 管理员可以查看
  447. return true if current_user.can_approve_events?
  448. false
  449. end
  450. # 获取审批队列位置
  451. def get_approval_queue_position(event)
  452. return nil unless event.pending_approval?
  453. ReadingEvent.where(approval_status: :pending)
  454. .where('submitted_for_approval_at <= ?', event.submitted_for_approval_at)
  455. .count
  456. end
  457. # 验证活动审批状态显示
  458. def validate_event_for_approval_display(event)
  459. validation_result = event.send(:validate_event_for_approval)
  460. {
  461. valid: validation_result[:valid],
  462. errors: validation_result[:errors],
  463. missing_fields: get_missing_required_fields(event)
  464. }
  465. end
  466. # 获取缺失的必填字段
  467. def get_missing_required_fields(event)
  468. missing_fields = []
  469. required_fields = [
  470. { field: :title, name: '活动标题' },
  471. { field: :book_name, name: '书籍名称' },
  472. { field: :description, name: '活动描述' },
  473. { field: :start_date, name: '开始日期' },
  474. { field: :end_date, name: '结束日期' },
  475. { field: :max_participants, name: '最大参与人数' }
  476. ]
  477. required_fields.each do |field_config|
  478. value = event.send(field_config[:field])
  479. if value.blank?
  480. missing_fields << {
  481. field: field_config[:field],
  482. name: field_config[:name],
  483. current_value: value
  484. }
  485. end
  486. end
  487. # 检查费用相关字段(如果是收费活动)
  488. if event.fee_type != 'free'
  489. fee_fields = [
  490. { field: :fee_amount, name: '费用金额' },
  491. { field: :leader_reward_percentage, name: '领读人奖励比例' }
  492. ]
  493. fee_fields.each do |field_config|
  494. value = event.send(field_config[:field])
  495. if value.blank? || value.to_f <= 0
  496. missing_fields << {
  497. field: field_config[:field],
  498. name: field_config[:name],
  499. current_value: value
  500. }
  501. end
  502. end
  503. end
  504. missing_fields
  505. end
  506. # 格式化用户信息
  507. def user_info(user)
  508. return nil unless user
  509. {
  510. id: user.id,
  511. nickname: user.nickname,
  512. avatar_url: user.avatar_url
  513. }
  514. end
  515. # 安全整数参数转换
  516. def safe_integer_param(param)
  517. return nil if param.blank?
  518. Integer(param)
  519. rescue ArgumentError, TypeError
  520. nil
  521. end
  522. # 解析日期参数
  523. def parse_date_param(param)
  524. return nil if param.blank?
  525. Date.parse(param.to_s)
  526. rescue ArgumentError, TypeError
  527. nil
  528. end
  529. end

app/controllers/api/v1/base_controller.rb

0.0% lines covered

231 relevant lines. 0 lines covered and 231 lines missed.
    
  1. require 'digest'
  2. class Api::V1::BaseController < ActionController::API
  3. # 添加JSON支持
  4. include ActionController::MimeResponds
  5. # 添加API版本控制
  6. include ApiVersionable
  7. # 添加全局错误处理
  8. include GlobalErrorHandler
  9. # 添加API响应格式化
  10. include ApiResponseFormatter
  11. # 添加请求验证
  12. include RequestValidator
  13. # 添加API安全增强
  14. include ApiSecurity
  15. # 添加用户体验增强
  16. include UserExperienceEnhancer
  17. private
  18. # 统一成功响应格式 - 使用 ApiResponseService
  19. def render_success(data: nil, message: '操作成功', meta: {}, status_code: 200)
  20. response, status = ApiResponseService.success_response(
  21. data: data,
  22. message: message,
  23. meta: meta,
  24. status_code: status_code
  25. )
  26. render json: response, status: status
  27. end
  28. # 统一错误响应格式 - 使用 ApiResponseService
  29. def render_error(message: '操作失败', error_code: nil, details: {}, status_code: 400)
  30. response, status = ApiResponseService.error_response(
  31. message: message,
  32. error_code: error_code,
  33. details: details,
  34. status_code: status_code
  35. )
  36. render json: response, status: status
  37. end
  38. # 验证错误响应
  39. def render_validation_error(errors, message: '请求参数验证失败')
  40. response, status = ApiResponseService.validation_error_response(errors, message: message)
  41. render json: response, status: status
  42. end
  43. # 未找到错误响应
  44. def render_not_found(resource_type: '资源', resource_id: nil)
  45. response, status = ApiResponseService.not_found_response(
  46. resource_type: resource_type,
  47. resource_id: resource_id
  48. )
  49. render json: response, status: status
  50. end
  51. # 权限错误响应
  52. def render_authorization_error(message: '权限不足', required_permission: nil)
  53. response, status = ApiResponseService.authorization_error_response(
  54. message: message,
  55. required_permission: required_permission
  56. )
  57. render json: response, status: status
  58. end
  59. # 认证错误响应
  60. def render_authentication_error(message: '认证失败', details: {})
  61. response, status = ApiResponseService.authentication_error_response(
  62. message: message,
  63. details: details
  64. )
  65. render json: response, status: status
  66. end
  67. # 服务不可用错误响应
  68. def render_service_unavailable(service_name: '服务', retry_after: 30)
  69. response, status = ApiResponseService.service_unavailable_response(
  70. service_name: service_name,
  71. retry_after: retry_after
  72. )
  73. render json: response, status: status
  74. end
  75. # 限流错误响应
  76. def render_rate_limit_error(limit_info = {})
  77. response, status = ApiResponseService.rate_limit_error_response(limit_info)
  78. render json: response, status: status
  79. end
  80. # 分页响应
  81. def render_paginated(records:, pagination:, message: '获取成功', additional_meta: {})
  82. response, status = ApiResponseService.paginated_response(
  83. records: records,
  84. pagination: pagination,
  85. message: message,
  86. additional_meta: additional_meta
  87. )
  88. render json: response, status: status
  89. end
  90. # 创建成功响应
  91. def render_create_success(resource, resource_name: '资源')
  92. response, status = ApiResponseService.create_success_response(
  93. resource,
  94. resource_name: resource_name
  95. )
  96. render json: response, status: status
  97. end
  98. # 更新成功响应
  99. def render_update_success(resource, resource_name: '资源')
  100. response, status = ApiResponseService.update_success_response(
  101. resource,
  102. resource_name: resource_name
  103. )
  104. render json: response, status: status
  105. end
  106. # 删除成功响应
  107. def render_destroy_success(resource_name: '资源')
  108. response, status = ApiResponseService.destroy_success_response(
  109. resource_name: resource_name
  110. )
  111. render json: response, status: status
  112. end
  113. # 批量操作响应
  114. def render_batch_operation(results, operation_name: '批量操作')
  115. response, status = ApiResponseService.batch_operation_response(
  116. results,
  117. operation_name: operation_name
  118. )
  119. render json: response, status: status
  120. end
  121. # 健康检查响应
  122. def render_health_check(additional_info = {})
  123. response, status = ApiResponseService.health_response(additional_info)
  124. render json: response, status: status
  125. end
  126. # 当前用户认证
  127. def authenticate_user!
  128. auth_header = request.headers['Authorization']
  129. token = auth_header&.split(' ')&.last
  130. unless token
  131. render_authentication_error(
  132. message: '请先登录',
  133. details: { reason: 'missing_token', required_format: 'Bearer <token>' }
  134. )
  135. return false
  136. end
  137. decoded = User.decode_jwt_token(token)
  138. unless decoded
  139. render_authentication_error(
  140. message: '认证令牌无效',
  141. details: { reason: 'invalid_token', token_provided: token[0..20] + '...' }
  142. )
  143. return false
  144. end
  145. @current_user = User.find_by(id: decoded['user_id'])
  146. unless @current_user
  147. render_authentication_error(
  148. message: '用户不存在',
  149. details: { reason: 'user_not_found', user_id: decoded['user_id'] }
  150. )
  151. return false
  152. end
  153. true
  154. rescue => e
  155. Rails.logger.error "Authentication error: #{e.message}"
  156. render_authentication_error(
  157. message: '认证失败',
  158. details: { reason: 'processing_error', error: e.message }
  159. )
  160. false
  161. end
  162. # 权限检查 - 必须是活动创建者
  163. def authorize_event_leader!
  164. unless @current_user == @reading_event&.leader
  165. render_authorization_error(
  166. message: '权限不足,只有活动创建者可以执行此操作',
  167. required_permission: 'event_leader'
  168. )
  169. end
  170. end
  171. # 权限检查 - 必须是管理员
  172. def authorize_admin!
  173. unless @current_user&.admin?
  174. render_authorization_error(
  175. message: '权限不足,只有管理员可以执行此操作',
  176. required_permission: 'admin_access'
  177. )
  178. end
  179. end
  180. # 分页参数处理
  181. def pagination_params
  182. {
  183. page: params[:page]&.to_i || 1,
  184. per_page: [params[:per_page]&.to_i || 20, 100].min
  185. }
  186. end
  187. # 排序参数处理
  188. def sorting_params(default_field: :created_at, default_direction: :desc)
  189. {
  190. sort_field: params[:sort]&.to_sym || default_field,
  191. sort_direction: params[:direction]&.to_sym || default_direction
  192. }
  193. end
  194. # 构建分页元数据
  195. def pagination_meta(collection)
  196. {
  197. current_page: collection.current_page,
  198. per_page: collection.limit_value,
  199. total_pages: collection.total_pages,
  200. total_count: collection.total_count,
  201. next_page: collection.next_page,
  202. prev_page: collection.prev_page
  203. }
  204. end
  205. # 安全的参数检查
  206. def safe_integer_param(param_name, default_value: 0)
  207. value = params[param_name]
  208. return default_value if value.blank?
  209. begin
  210. value.to_i
  211. rescue ArgumentError, TypeError
  212. default_value
  213. end
  214. end
  215. def safe_decimal_param(param_name, default_value: 0.0)
  216. value = params[param_name]
  217. return default_value if value.blank?
  218. begin
  219. BigDecimal(value.to_s)
  220. rescue ArgumentError, TypeError
  221. default_value
  222. end
  223. end
  224. def safe_date_param(param_name)
  225. value = params[param_name]
  226. return nil if value.blank?
  227. begin
  228. Date.parse(value)
  229. rescue ArgumentError
  230. nil
  231. end
  232. end
  233. def safe_datetime_param(param_name)
  234. value = params[param_name]
  235. return nil if value.blank?
  236. begin
  237. DateTime.parse(value)
  238. rescue ArgumentError
  239. nil
  240. end
  241. end
  242. # 当前用户
  243. def current_user
  244. @current_user
  245. end
  246. # 记录API调用日志
  247. def log_api_call(action, result = 'success')
  248. Rails.logger.info "API Call: #{action} by User #{current_user&.id} - #{result}"
  249. end
  250. # 参数验证辅助方法
  251. def validate_required_fields(*fields)
  252. missing_fields = fields.select { |field| params[field].blank? }
  253. if missing_fields.any?
  254. render_validation_error(
  255. missing_fields.map { |field| "#{field} 不能为空" }.to_hash,
  256. message: '缺少必要参数'
  257. )
  258. return false
  259. end
  260. true
  261. end
  262. end

app/controllers/api/v1/check_ins_controller.rb

0.0% lines covered

590 relevant lines. 0 lines covered and 590 lines missed.
    
  1. class Api::V1::CheckInsController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :set_check_in, only: [:show, :update, :destroy]
  4. before_action :check_check_in_permission, only: [:update, :destroy]
  5. # POST /api/v1/reading_schedules/:reading_schedule_id/check_ins
  6. # 创建打卡
  7. def create
  8. schedule_id = params[:reading_schedule_id]
  9. schedule = ReadingSchedule.find_by(id: schedule_id)
  10. unless schedule
  11. render_error(
  12. message: '阅读计划不存在',
  13. code: 'SCHEDULE_NOT_FOUND',
  14. status: :not_found
  15. )
  16. return
  17. end
  18. # 检查用户是否已报名该活动
  19. enrollment = current_user.event_enrollments.find_by(reading_event: schedule.reading_event)
  20. unless enrollment
  21. render_error(
  22. message: '您还未报名该活动',
  23. code: 'NOT_ENROLLED',
  24. status: :forbidden
  25. )
  26. return
  27. end
  28. # 检查活动状态
  29. unless schedule.reading_event.in_progress?
  30. render_error(
  31. message: '活动尚未开始或已结束',
  32. code: 'EVENT_NOT_ACTIVE',
  33. status: :unprocessable_entity
  34. )
  35. return
  36. end
  37. # 检查是否已经打卡
  38. existing_check_in = CheckIn.find_by(
  39. user: current_user,
  40. reading_schedule: schedule
  41. )
  42. if existing_check_in
  43. render_error(
  44. message: '今日已打卡',
  45. code: 'ALREADY_CHECKED_IN',
  46. status: :unprocessable_entity
  47. )
  48. return
  49. end
  50. # 检查打卡时间窗口
  51. unless can_check_in?(schedule)
  52. render_error(
  53. message: '打卡时间已过,请使用补卡功能',
  54. code: 'CHECK_IN_TIME_EXPIRED',
  55. status: :unprocessable_entity
  56. )
  57. return
  58. end
  59. check_in = CheckIn.new(check_in_params)
  60. check_in.user = current_user
  61. check_in.reading_schedule = schedule
  62. check_in.enrollment = enrollment
  63. if check_in.save
  64. render_success(
  65. data: check_in_response_data(check_in),
  66. message: '打卡成功'
  67. )
  68. log_api_call('check_ins#create')
  69. else
  70. render_error(
  71. message: '打卡失败',
  72. errors: check_in.errors.full_messages,
  73. code: 'CHECK_IN_FAILED',
  74. status: :unprocessable_entity
  75. )
  76. end
  77. rescue ActiveRecord::RecordNotFound
  78. render_error(
  79. message: '阅读计划不存在',
  80. code: 'SCHEDULE_NOT_FOUND',
  81. status: :not_found
  82. )
  83. rescue => e
  84. render_error(
  85. message: '打卡失败',
  86. errors: [e.message],
  87. code: 'CHECK_IN_ERROR'
  88. )
  89. end
  90. # GET /api/v1/reading_schedules/:reading_schedule_id/check_ins
  91. # 获取打卡列表
  92. def index
  93. schedule_id = params[:reading_schedule_id]
  94. schedule = ReadingSchedule.find_by(id: schedule_id)
  95. unless schedule
  96. render_error(
  97. message: '阅读计划不存在',
  98. code: 'SCHEDULE_NOT_FOUND',
  99. status: :not_found
  100. )
  101. return
  102. end
  103. # 检查权限
  104. unless can_view_check_ins?(schedule)
  105. render_error(
  106. message: '权限不足',
  107. code: 'FORBIDDEN',
  108. status: :forbidden
  109. )
  110. return
  111. end
  112. # 分页参数
  113. page = safe_integer_param(params[:page]) || 1
  114. per_page = safe_integer_param(params[:per_page]) || 20
  115. check_ins = schedule.check_ins.includes(:user, :flowers, :comments)
  116. .order(submitted_at: :desc)
  117. .page(page)
  118. .per(per_page)
  119. render_success(
  120. data: {
  121. check_ins: check_ins.map { |ci| check_in_response_data(ci) },
  122. pagination: {
  123. current_page: page,
  124. per_page: per_page,
  125. total_pages: check_ins.total_pages,
  126. total_count: check_ins.total_count
  127. },
  128. schedule_info: schedule_basic_info(schedule),
  129. statistics: schedule_check_in_statistics(schedule)
  130. }
  131. )
  132. rescue ActiveRecord::RecordNotFound
  133. render_error(
  134. message: '阅读计划不存在',
  135. code: 'SCHEDULE_NOT_FOUND',
  136. status: :not_found
  137. )
  138. rescue => e
  139. render_error(
  140. message: '获取打卡列表失败',
  141. errors: [e.message],
  142. code: 'GET_CHECK_INS_ERROR'
  143. )
  144. end
  145. # GET /api/v1/check_ins/:id
  146. # 获取打卡详情
  147. def show
  148. format_content = params[:format_content] == 'true'
  149. render_success(
  150. data: check_in_response_data(@check_in, detailed: true, format_content: format_content)
  151. )
  152. rescue => e
  153. render_error(
  154. message: '获取打卡详情失败',
  155. errors: [e.message],
  156. code: 'GET_CHECK_IN_ERROR'
  157. )
  158. end
  159. # PUT /api/v1/check_ins/:id
  160. # 更新打卡
  161. def update
  162. # 检查打卡是否可以编辑
  163. unless @check_in.can_be_edited?
  164. render_error(
  165. message: '活动已结束,无法编辑打卡',
  166. code: 'CANNOT_EDIT',
  167. status: :unprocessable_entity
  168. )
  169. return
  170. end
  171. # 检查是否已获得小红花,如果有则给出警告
  172. if @check_in.flowers.any?
  173. render_error(
  174. message: '已获得小红花的打卡修改可能会影响小红花发放者的统计,请谨慎操作。是否继续修改?',
  175. code: 'HAS_FLOWERS_WARNING',
  176. status: :unprocessable_entity
  177. )
  178. return
  179. end
  180. if @check_in.update(check_in_params)
  181. render_success(
  182. data: check_in_response_data(@check_in),
  183. message: '打卡更新成功'
  184. )
  185. log_api_call('check_ins#update')
  186. else
  187. render_error(
  188. message: '打卡更新失败',
  189. errors: @check_in.errors.full_messages,
  190. code: 'UPDATE_CHECK_IN_FAILED',
  191. status: :unprocessable_entity
  192. )
  193. end
  194. rescue => e
  195. render_error(
  196. message: '打卡更新失败',
  197. errors: [e.message],
  198. code: 'UPDATE_CHECK_IN_ERROR'
  199. )
  200. end
  201. # DELETE /api/v1/check_ins/:id
  202. # 删除打卡
  203. def destroy
  204. # 只能删除自己的打卡
  205. unless @check_in.user == current_user
  206. render_error(
  207. message: '只能删除自己的打卡',
  208. code: 'CANNOT_DELETE_OTHERS',
  209. status: :forbidden
  210. )
  211. return
  212. end
  213. # 检查打卡是否可以删除
  214. unless @check_in.can_be_deleted?
  215. render_error(
  216. message: '活动已结束,无法删除打卡',
  217. code: 'CANNOT_DELETE',
  218. status: :unprocessable_entity
  219. )
  220. return
  221. end
  222. # 检查是否已获得小红花,如果有则给出警告
  223. if @check_in.flowers.any?
  224. flowers_count = @check_in.flowers.count
  225. render_error(
  226. message: "该打卡已获得#{flowers_count}朵小红花,删除后将同时删除这些小红花记录,是否确认删除?",
  227. code: 'HAS_FLOWERS_WARNING',
  228. status: :unprocessable_entity
  229. )
  230. return
  231. end
  232. if @check_in.destroy
  233. render_success(
  234. message: '打卡删除成功,相关统计数据已更新'
  235. )
  236. log_api_call('check_ins#destroy')
  237. else
  238. render_error(
  239. message: '打卡删除失败',
  240. code: 'DELETE_CHECK_IN_FAILED',
  241. status: :unprocessable_entity
  242. )
  243. end
  244. rescue => e
  245. render_error(
  246. message: '打卡删除失败',
  247. errors: [e.message],
  248. code: 'DELETE_CHECK_IN_ERROR'
  249. )
  250. end
  251. # POST /api/v1/check_ins/:id/submit_late
  252. # 提交迟到打卡
  253. def submit_late
  254. # 检查是否可以编辑(活动是否已结束)
  255. unless @check_in.can_be_edited?
  256. render_error(
  257. message: '活动已结束,无法提交迟到打卡',
  258. code: 'CANNOT_SUBMIT_LATE',
  259. status: :unprocessable_entity
  260. )
  261. return
  262. end
  263. # 更新状态为迟到
  264. if @check_in.update(status: :late)
  265. render_success(
  266. data: check_in_response_data(@check_in),
  267. message: '迟到打卡提交成功'
  268. )
  269. log_api_call('check_ins#submit_late')
  270. else
  271. render_error(
  272. message: '迟到打卡提交失败',
  273. errors: @check_in.errors.full_messages,
  274. code: 'SUBMIT_LATE_FAILED',
  275. status: :unprocessable_entity
  276. )
  277. end
  278. rescue => e
  279. render_error(
  280. message: '迟到打卡提交失败',
  281. errors: [e.message],
  282. code: 'SUBMIT_LATE_ERROR'
  283. )
  284. end
  285. # POST /api/v1/check_ins/:id/submit_supplement
  286. # 提交补卡
  287. def submit_supplement
  288. # 检查是否可以编辑(活动是否已结束)
  289. unless @check_in.can_be_edited?
  290. render_error(
  291. message: '活动已结束,无法提交补卡',
  292. code: 'CANNOT_MAKEUP',
  293. status: :unprocessable_entity
  294. )
  295. return
  296. end
  297. # 检查是否可以补卡(基于日期和活动状态)
  298. unless @check_in.can_makeup?
  299. render_error(
  300. message: '该打卡不适用补卡功能',
  301. code: 'CANNOT_MAKEUP',
  302. status: :unprocessable_entity
  303. )
  304. return
  305. end
  306. # 更新状态为补卡
  307. if @check_in.update(status: :supplement)
  308. render_success(
  309. data: check_in_response_data(@check_in),
  310. message: '补卡提交成功'
  311. )
  312. log_api_call('check_ins#submit_supplement')
  313. else
  314. render_error(
  315. message: '补卡提交失败',
  316. errors: @check_in.errors.full_messages,
  317. code: 'SUBMIT_SUPPLEMENT_FAILED',
  318. status: :unprocessable_entity
  319. )
  320. end
  321. rescue => e
  322. render_error(
  323. message: '补卡提交失败',
  324. errors: [e.message],
  325. code: 'SUBMIT_SUPPLEMENT_ERROR'
  326. )
  327. end
  328. # GET /api/v1/users/:user_id/check_ins
  329. # 获取用户的打卡记录
  330. def user_check_ins
  331. user_id = safe_integer_param(params[:user_id])
  332. user = user_id ? User.find_by(id: user_id) : current_user
  333. unless user
  334. render_error(
  335. message: '用户不存在',
  336. code: 'USER_NOT_FOUND',
  337. status: :not_found
  338. )
  339. return
  340. end
  341. # 检查权限(只能查看自己的打卡,除非是管理员)
  342. unless user == current_user || current_user.can_approve_events?
  343. render_error(
  344. message: '权限不足',
  345. code: 'FORBIDDEN',
  346. status: :forbidden
  347. )
  348. return
  349. end
  350. # 分页参数
  351. page = safe_integer_param(params[:page]) || 1
  352. per_page = safe_integer_param(params[:per_page]) || 20
  353. # 筛选参数
  354. status_filter = params[:status]
  355. start_date = parse_date_param(params[:start_date])
  356. end_date = parse_date_param(params[:end_date])
  357. check_ins = user.check_ins.includes(:reading_schedule, :flowers, :comments)
  358. .joins(:reading_schedule)
  359. # 应用筛选条件
  360. check_ins = check_ins.where(status: status_filter) if status_filter.present?
  361. check_ins = check_ins.where('reading_schedules.date >= ?', start_date) if start_date.present?
  362. check_ins = check_ins.where('reading_schedules.date <= ?', end_date) if end_date.present?
  363. check_ins = check_ins.order(submitted_at: :desc)
  364. .page(page)
  365. .per(per_page)
  366. render_success(
  367. data: {
  368. check_ins: check_ins.map { |ci| check_in_response_data(ci) },
  369. pagination: {
  370. current_page: page,
  371. per_page: per_page,
  372. total_pages: check_ins.total_pages,
  373. total_count: check_ins.total_count
  374. },
  375. user: user_info(user),
  376. statistics: user_check_in_statistics(user)
  377. }
  378. )
  379. rescue ActiveRecord::RecordNotFound
  380. render_error(
  381. message: '用户不存在',
  382. code: 'USER_NOT_FOUND',
  383. status: :not_found
  384. )
  385. rescue => e
  386. render_error(
  387. message: '获取用户打卡记录失败',
  388. errors: [e.message],
  389. code: 'GET_USER_CHECK_INS_ERROR'
  390. )
  391. end
  392. # GET /api/v1/check_ins/statistics
  393. # 获取打卡统计
  394. def statistics
  395. # 统计参数
  396. event_id = safe_integer_param(params[:event_id])
  397. schedule_id = safe_integer_param(params[:schedule_id])
  398. date_range = params[:date_range] # today, week, month
  399. base_query = CheckIn.includes(:user, :reading_schedule)
  400. # 应用筛选条件
  401. if event_id
  402. event = ReadingEvent.find_by(id: event_id)
  403. base_query = base_query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: event_id })
  404. end
  405. if schedule_id
  406. base_query = base_query.where(reading_schedule_id: schedule_id)
  407. end
  408. case date_range
  409. when 'today'
  410. base_query = base_query.joins(:reading_schedule).where(reading_schedules: { date: Date.current })
  411. when 'week'
  412. base_query = base_query.joins(:reading_schedule).where(reading_schedules: { date: Date.current.beginning_of_week..Date.current.end_of_week })
  413. when 'month'
  414. base_query = base_query.joins(:reading_schedule).where(reading_schedules: { date: Date.current.beginning_of_month..Date.current.end_of_month })
  415. end
  416. total_check_ins = base_query.count
  417. normal_check_ins = base_query.where(status: :normal).count
  418. supplement_check_ins = base_query.where(status: :supplement).count
  419. late_check_ins = base_query.where(status: :late).count
  420. # 用户统计
  421. user_stats = base_query.group(:user_id).count
  422. active_users = user_stats.size
  423. # 内容统计
  424. total_words = base_query.sum(:word_count)
  425. avg_words = total_check_ins > 0 ? (total_words.to_f / total_check_ins).round(2) : 0
  426. # 小红花统计
  427. flowers_stats = base_query.joins(:flowers).group(:check_in_id).count
  428. render_success(
  429. data: {
  430. total_check_ins: total_check_ins,
  431. normal_check_ins: normal_check_ins,
  432. supplement_check_ins: supplement_check_ins,
  433. late_check_ins: late_check_ins,
  434. active_users: active_users,
  435. total_words: total_words,
  436. average_words: avg_words,
  437. flowers_given: flowers_stats.size,
  438. date_range: date_range,
  439. event_id: event_id,
  440. schedule_id: schedule_id
  441. }
  442. )
  443. rescue => e
  444. render_error(
  445. message: '获取打卡统计失败',
  446. errors: [e.message],
  447. code: 'GET_CHECK_INS_STATISTICS_ERROR'
  448. )
  449. end
  450. private
  451. def set_check_in
  452. @check_in = CheckIn.find(params[:id])
  453. rescue ActiveRecord::RecordNotFound
  454. render_error(
  455. message: '打卡记录不存在',
  456. code: 'CHECK_IN_NOT_FOUND',
  457. status: :not_found
  458. )
  459. end
  460. def check_check_in_permission
  461. unless @check_in.user == current_user
  462. render_error(
  463. message: '只能操作自己的打卡',
  464. code: 'PERMISSION_DENIED',
  465. status: :forbidden
  466. )
  467. end
  468. end
  469. def can_check_in?(schedule)
  470. return true unless schedule # 防止nil错误
  471. schedule_date = schedule.date
  472. current_time = Time.current
  473. # 当天的打卡可以在晚上11:59前提交
  474. if schedule_date == Date.current
  475. return current_time <= schedule_date.to_time.end_of_day
  476. end
  477. # 过去的日期可以补卡
  478. schedule_date < Date.current && schedule.reading_event.in_progress?
  479. end
  480. def can_view_check_ins?(schedule)
  481. # 活动参与者可以查看
  482. return true if current_user.enrolled?(schedule.reading_event)
  483. # 活动创建者可以查看
  484. return true if schedule.reading_event.leader == current_user
  485. # 领读人可以查看
  486. if schedule.daily_leader == current_user || schedule.reading_event.current_daily_leader?(current_user, schedule)
  487. return true
  488. end
  489. # 管理员可以查看
  490. current_user.can_approve_events?
  491. end
  492. def check_in_params
  493. params.require(:check_in).permit(:content, :word_count, :status)
  494. end
  495. def check_in_response_data(check_in, detailed: false, format_content: false)
  496. base_data = {
  497. id: check_in.id,
  498. user: user_info(check_in.user),
  499. reading_schedule: {
  500. id: check_in.reading_schedule.id,
  501. day_number: check_in.reading_schedule.day_number,
  502. date: check_in.reading_schedule.date,
  503. reading_progress: check_in.reading_schedule.reading_progress
  504. },
  505. content: check_in.content,
  506. formatted_content: format_content ? check_in.formatted_content : nil,
  507. content_preview: check_in.content_preview(150),
  508. word_count: check_in.word_count,
  509. status: check_in.status,
  510. submitted_at: check_in.submitted_at,
  511. updated_at: check_in.updated_at,
  512. flowers_count: check_in.flowers_count,
  513. engagement_score: check_in.engagement_score,
  514. quality_score: check_in.quality_score,
  515. keywords: check_in.keywords(5),
  516. reading_time: check_in.reading_time_estimate,
  517. can_be_edited: check_in.can_be_edited?,
  518. can_receive_flowers: check_in.can_receive_flowers?,
  519. high_quality: check_in.high_quality?,
  520. has_formatting_issues: check_in.has_formatting_issues?,
  521. contains_sensitive_words: check_in.contains_sensitive_words?
  522. }
  523. if detailed
  524. base_data[:flowers] = check_in.flowers.map { |flower| flower_response_data(flower) }
  525. base_data[:comments_count] = check_in.comments.count
  526. base_data[:reading_event] = {
  527. id: check_in.reading_event&.id,
  528. title: check_in.reading_event&.title
  529. }
  530. base_data[:enrollment] = {
  531. id: check_in.enrollment&.id,
  532. completion_rate: check_in.enrollment&.completion_rate
  533. }
  534. base_data[:content_summary] = check_in.content_summary(200)
  535. base_data[:compliance_check] = check_in.compliance_check
  536. end
  537. base_data
  538. end
  539. def flower_response_data(flower)
  540. {
  541. id: flower.id,
  542. giver: user_info(flower.giver),
  543. comment: flower.comment,
  544. amount: flower.amount,
  545. created_at: flower.created_at
  546. }
  547. end
  548. def user_info(user)
  549. return nil unless user
  550. {
  551. id: user.id,
  552. nickname: user.nickname,
  553. avatar_url: user.avatar_url
  554. }
  555. end
  556. def schedule_basic_info(schedule)
  557. {
  558. id: schedule.id,
  559. day_number: schedule.day_number,
  560. date: schedule.date,
  561. reading_progress: schedule.reading_progress,
  562. daily_leader: schedule.daily_leader ? user_info(schedule.daily_leader) : nil
  563. }
  564. end
  565. def schedule_check_in_statistics(schedule)
  566. check_ins = schedule.check_ins
  567. total = check_ins.count
  568. today = check_ins.today.count
  569. {
  570. total: total,
  571. today: today,
  572. normal: check_ins.normal.count,
  573. supplement: check_ins.supplement.count,
  574. late: check_ins.late.count,
  575. total_words: check_ins.sum(:word_count),
  576. average_words: total > 0 ? (check_ins.sum(:word_count).to_f / total).round(2) : 0,
  577. flowers_given: check_ins.joins(:flowers).count
  578. }
  579. end
  580. def user_check_in_statistics(user)
  581. check_ins = user.check_ins.includes(:reading_schedule)
  582. total_check_ins = check_ins.count
  583. this_month = check_ins.joins(:reading_schedule)
  584. .where('reading_schedules.date >= ?', Date.current.beginning_of_month)
  585. .count
  586. {
  587. total_check_ins: total_check_ins,
  588. this_month: this_month,
  589. current_streak: calculate_current_streak(user),
  590. longest_streak: calculate_longest_streak(user),
  591. total_words: check_ins.sum(:word_count),
  592. average_words: total_check_ins > 0 ? (check_ins.sum(:word_count).to_f / total_check_ins).round(2) : 0,
  593. flowers_received: user.flowers_received_count || 0,
  594. engagement_score: user.check_ins.average(:engagement_score)&.round(2) || 0
  595. }
  596. end
  597. def calculate_current_streak(user)
  598. # 计算当前连续打卡天数
  599. streak = 0
  600. date = Date.current
  601. while date >= Date.current - 30.days # 最多计算30天
  602. if user.check_ins.joins(:reading_schedule).where('reading_schedules.date = ?', date).exists?
  603. streak += 1
  604. date -= 1.day
  605. else
  606. break
  607. end
  608. end
  609. streak
  610. end
  611. def calculate_longest_streak(user)
  612. # 计算历史最长连续打卡天数
  613. # 这里可以优化为更高效的算法
  614. check_in_dates = user.check_ins.joins(:reading_schedule)
  615. .pluck('reading_schedules.date')
  616. .sort.uniq
  617. return 0 if check_in_dates.empty?
  618. longest_streak = 1
  619. current_streak = 1
  620. check_in_dates.each_cons do |date1, date2|
  621. if date2 == date1 + 1.day
  622. current_streak += 1
  623. longest_streak = [longest_streak, current_streak].max
  624. else
  625. current_streak = 1
  626. end
  627. end
  628. longest_streak
  629. end
  630. # 辅助方法
  631. def safe_integer_param(param)
  632. return nil if param.blank?
  633. Integer(param)
  634. rescue ArgumentError, TypeError
  635. nil
  636. end
  637. def parse_date_param(param)
  638. return nil if param.blank?
  639. Date.parse(param.to_s)
  640. rescue ArgumentError, TypeError
  641. nil
  642. end
  643. end

app/controllers/api/v1/content_export_controller.rb

0.0% lines covered

309 relevant lines. 0 lines covered and 309 lines missed.
    
  1. class Api::V1::ContentExportController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. # GET /api/v1/content_export/statistics
  4. # 导出统计信息
  5. def statistics
  6. export_params = build_export_params
  7. stats = ContentExportService.export_statistics(export_params)
  8. render_success(
  9. data: stats,
  10. message: '导出统计信息获取成功'
  11. )
  12. rescue => e
  13. render_error(
  14. message: '获取导出统计信息失败',
  15. errors: [e.message],
  16. code: 'EXPORT_STATISTICS_ERROR'
  17. )
  18. end
  19. # GET /api/v1/content_export/preview
  20. # 导出预览
  21. def preview
  22. export_params = build_export_params
  23. # 限制预览数量
  24. export_params[:limit] = 5
  25. check_ins = get_check_ins_for_preview(export_params)
  26. render_success(
  27. data: {
  28. check_ins: check_ins.map(&:to_search_result_h),
  29. total_count: estimate_total_count(export_params),
  30. preview: true,
  31. limit: 5
  32. },
  33. message: '导出预览生成成功'
  34. )
  35. rescue => e
  36. render_error(
  37. message: '生成导出预览失败',
  38. errors: [e.message],
  39. code: 'EXPORT_PREVIEW_ERROR'
  40. )
  41. end
  42. # GET /api/v1/content_export/export
  43. # 执行导出
  44. def export
  45. export_params = build_export_params
  46. # 验证导出权限
  47. unless can_export_content?(export_params)
  48. render_error(
  49. message: '权限不足,无法导出这些内容',
  50. code: 'EXPORT_PERMISSION_DENIED',
  51. status: :forbidden
  52. )
  53. return
  54. end
  55. # 执行导出
  56. result = ContentExportService.export(export_params)
  57. if result.success?
  58. # 记录导出操作
  59. log_export_operation(export_params, result)
  60. send_data result.content,
  61. filename: result.filename,
  62. type: result.content_type,
  63. disposition: 'attachment'
  64. else
  65. render_error(
  66. message: '导出失败',
  67. errors: [result.content],
  68. code: 'EXPORT_FAILED'
  69. )
  70. end
  71. rescue => e
  72. render_error(
  73. message: '导出过程中发生错误',
  74. errors: [e.message],
  75. code: 'EXPORT_ERROR'
  76. )
  77. end
  78. # POST /api/v1/content_export/batch_export
  79. # 批量导出
  80. def batch_export
  81. export_requests = params[:export_requests]
  82. unless export_requests.is_a?(Array) && export_requests.any?
  83. render_error(
  84. message: '请提供导出请求列表',
  85. code: 'INVALID_EXPORT_REQUESTS',
  86. status: :unprocessable_entity
  87. )
  88. return
  89. end
  90. # 验证批量导出权限
  91. unless current_user.can_approve_events? # 只有管理员可以批量导出
  92. render_error(
  93. message: '权限不足,只有管理员可以批量导出',
  94. code: 'BATCH_EXPORT_PERMISSION_DENIED',
  95. status: :forbidden
  96. )
  97. return
  98. end
  99. # 执行批量导出
  100. results = ContentExportService.batch_export(export_requests)
  101. # 记录批量导出操作
  102. log_batch_export_operation(export_requests, results)
  103. render_success(
  104. data: {
  105. results: results.map { |result| export_result_to_h(result) },
  106. total_requests: export_requests.count,
  107. successful_exports: results.count(&:success?),
  108. failed_exports: results.count { |r| !r.success? }
  109. },
  110. message: '批量导出完成'
  111. )
  112. rescue => e
  113. render_error(
  114. message: '批量导出过程中发生错误',
  115. errors: [e.message],
  116. code: 'BATCH_EXPORT_ERROR'
  117. )
  118. end
  119. # GET /api/v1/content_export/templates
  120. # 获取导出模板
  121. def templates
  122. templates = [
  123. {
  124. id: 'personal',
  125. name: '个人打卡记录',
  126. description: '导出当前用户的所有打卡记录',
  127. params: {
  128. format: 'pdf',
  129. include_metadata: true,
  130. include_comments: true,
  131. include_flowers: true,
  132. sort_by: 'created_at',
  133. sort_direction: 'desc'
  134. }
  135. },
  136. {
  137. id: 'event_summary',
  138. name: '活动汇总报告',
  139. description: '导出指定活动的所有打卡记录汇总',
  140. params: {
  141. format: 'pdf',
  142. include_metadata: true,
  143. include_comments: false,
  144. include_flowers: true,
  145. sort_by: 'created_at',
  146. sort_direction: 'asc'
  147. }
  148. },
  149. {
  150. id: 'quality_content',
  151. name: '高质量内容精选',
  152. description: '导出所有高质量打卡内容',
  153. params: {
  154. format: 'markdown',
  155. include_metadata: true,
  156. include_comments: true,
  157. include_flowers: true,
  158. sort_by: 'quality_score',
  159. sort_direction: 'desc'
  160. }
  161. },
  162. {
  163. id: 'data_analysis',
  164. name: '数据分析报告',
  165. description: '导出用于数据分析的CSV格式数据',
  166. params: {
  167. format: 'csv',
  168. include_metadata: false,
  169. include_comments: false,
  170. include_flowers: true,
  171. sort_by: 'created_at',
  172. sort_direction: 'asc'
  173. }
  174. }
  175. ]
  176. render_success(
  177. data: templates,
  178. message: '导出模板获取成功'
  179. )
  180. end
  181. # POST /api/v1/content_export/save_template
  182. # 保存自定义模板
  183. def save_template
  184. template_name = params[:name]
  185. template_params = params[:template]
  186. if template_name.blank? || template_params.blank?
  187. render_error(
  188. message: '模板名称和参数不能为空',
  189. code: 'INVALID_TEMPLATE',
  190. status: :unprocessable_entity
  191. )
  192. return
  193. end
  194. # 这里可以实现保存模板到数据库的逻辑
  195. # 暂时返回成功响应
  196. render_success(
  197. message: '模板保存成功'
  198. )
  199. log_api_call('content_export#save_template')
  200. rescue => e
  201. render_error(
  202. message: '保存模板失败',
  203. errors: [e.message],
  204. code: 'SAVE_TEMPLATE_ERROR'
  205. )
  206. end
  207. # GET /api/v1/content_export/history
  208. # 导出历史
  209. def export_history
  210. limit = safe_integer_param(params[:limit]) || 20
  211. # 这里可以实现获取用户导出历史的逻辑
  212. # 暂时返回空数组
  213. history_items = []
  214. render_success(
  215. data: {
  216. history: history_items,
  217. limit: limit
  218. },
  219. message: '导出历史获取成功'
  220. )
  221. rescue => e
  222. render_error(
  223. message: '获取导出历史失败',
  224. errors: [e.message],
  225. code: 'EXPORT_HISTORY_ERROR'
  226. )
  227. end
  228. # POST /api/v1/content_export/schedule
  229. # 定时导出
  230. def schedule_export
  231. export_params = build_export_params
  232. schedule_time = params[:schedule_time]
  233. schedule_type = params[:schedule_type] || 'once' # once, daily, weekly, monthly
  234. unless schedule_time.present?
  235. render_error(
  236. message: '请提供导出时间',
  237. code: 'SCHEDULE_TIME_REQUIRED',
  238. status: :unprocessable_entity
  239. )
  240. return
  241. end
  242. # 这里可以实现定时导出的逻辑
  243. # 暂时返回成功响应
  244. render_success(
  245. message: '定时导出设置成功'
  246. )
  247. log_api_call('content_export#schedule_export')
  248. rescue => e
  249. render_error(
  250. message: '设置定时导出失败',
  251. errors: [e.message],
  252. code: 'SCHEDULE_EXPORT_ERROR'
  253. )
  254. end
  255. private
  256. # 构建导出参数
  257. def build_export_params
  258. permitted_params = params.permit(
  259. :format, :check_in_ids, :user_id, :event_id, :date_from, :date_to,
  260. :include_metadata, :include_comments, :include_flowers,
  261. :sort_by, :sort_direction, :template, :limit
  262. ).to_h
  263. # 设置默认值
  264. permitted_params[:format] ||= 'pdf'
  265. permitted_params[:include_metadata] = true if permitted_params[:include_metadata].nil?
  266. permitted_params[:sort_by] ||= 'created_at'
  267. permitted_params[:sort_direction] ||= 'desc'
  268. permitted_params
  269. end
  270. # 检查导出权限
  271. def can_export_content?(export_params)
  272. # 用户可以导出自己的内容
  273. return true if export_params[:user_id].blank? || export_params[:user_id] == current_user.id
  274. # 管理员可以导出任何内容
  275. return true if current_user.can_approve_events?
  276. # 活动领读人可以导出自己活动的内容
  277. if export_params[:event_id].present?
  278. event = ReadingEvent.find_by(id: export_params[:event_id])
  279. return true if event&.leader == current_user
  280. end
  281. false
  282. end
  283. # 获取预览用的打卡记录
  284. def get_check_ins_for_preview(export_params)
  285. query = CheckIn.includes(:user, :reading_schedule, :reading_event)
  286. # 应用筛选条件(简化版)
  287. if export_params[:user_id].present?
  288. query = query.where(user_id: export_params[:user_id])
  289. end
  290. if export_params[:event_id].present?
  291. query = query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: export_params[:event_id] })
  292. end
  293. if export_params[:date_from].present?
  294. query = query.where('check_ins.created_at >= ?', export_params[:date_from].beginning_of_day)
  295. end
  296. if export_params[:date_to].present?
  297. query = query.where('check_ins.created_at <= ?', export_params[:date_to].end_of_day)
  298. end
  299. # 限制数量并排序
  300. query.limit(export_params[:limit] || 5).order(created_at: :desc)
  301. end
  302. # 估算总数量
  303. def estimate_total_count(export_params)
  304. query = CheckIn.all
  305. # 应用相同的筛选条件
  306. if export_params[:user_id].present?
  307. query = query.where(user_id: export_params[:user_id])
  308. end
  309. if export_params[:event_id].present?
  310. query = query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: export_params[:event_id] })
  311. end
  312. if export_params[:date_from].present?
  313. query = query.where('check_ins.created_at >= ?', export_params[:date_from].beginning_of_day)
  314. end
  315. if export_params[:date_to].present?
  316. query = query.where('check_ins.created_at <= ?', export_params[:date_to].end_of_day)
  317. end
  318. query.count
  319. end
  320. # 记录导出操作
  321. def log_export_operation(export_params, result)
  322. # 这里可以实现导出操作的记录逻辑
  323. # 例如:保存到数据库、发送通知等
  324. log_api_call('content_export#export', {
  325. format: export_params[:format],
  326. check_ins_count: result.check_ins_count,
  327. filename: result.filename
  328. })
  329. end
  330. # 记录批量导出操作
  331. def log_batch_export_operation(export_requests, results)
  332. log_api_call('content_export#batch_export', {
  333. total_requests: export_requests.count,
  334. successful_exports: results.count(&:success?),
  335. failed_exports: results.count { |r| !r.success? }
  336. })
  337. end
  338. # 转换导出结果为哈希
  339. def export_result_to_h(result)
  340. {
  341. filename: result.filename,
  342. content_type: result.content_type,
  343. size: result.size,
  344. check_ins_count: result.check_ins_count,
  345. success: result.success?
  346. }
  347. end
  348. # 辅助方法
  349. def safe_integer_param(param)
  350. return nil if param.blank?
  351. Integer(param)
  352. rescue ArgumentError, TypeError
  353. nil
  354. end
  355. end

app/controllers/api/v1/content_reports_controller.rb

0.0% lines covered

398 relevant lines. 0 lines covered and 398 lines missed.
    
  1. class Api::V1::ContentReportsController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :set_check_in, only: [:create]
  4. before_action :set_report, only: [:show, :update]
  5. before_action :check_admin_permissions, only: [:index, :update, :batch_process, :statistics, :export]
  6. # POST /api/v1/content_reports
  7. # 创建举报
  8. def create
  9. reason = params[:reason]
  10. description = params[:description]
  11. if reason.blank?
  12. render_error(
  13. message: '请选择举报原因',
  14. code: 'MISSING_REASON',
  15. status: :unprocessable_entity
  16. )
  17. return
  18. end
  19. # 验证举报原因
  20. unless ContentReport.reasons.key?(reason.to_sym)
  21. render_error(
  22. message: '无效的举报原因',
  23. code: 'INVALID_REASON',
  24. status: :unprocessable_entity
  25. )
  26. return
  27. end
  28. # 创建举报
  29. result = ContentModerationService.create_report(
  30. current_user,
  31. @check_in,
  32. reason: reason.to_sym,
  33. description: description
  34. )
  35. if result[:success]
  36. render_success(
  37. data: content_report_response_data(result[:report]),
  38. message: result[:message]
  39. )
  40. log_api_call('content_reports#create')
  41. else
  42. render_error(
  43. message: result[:error],
  44. errors: result[:errors],
  45. code: 'REPORT_CREATE_FAILED',
  46. status: :unprocessable_entity
  47. )
  48. end
  49. rescue => e
  50. render_error(
  51. message: '提交举报失败',
  52. errors: [e.message],
  53. code: 'REPORT_CREATE_ERROR'
  54. )
  55. end
  56. # GET /api/v1/content_reports
  57. # 获取举报列表(管理员)
  58. def index
  59. page = safe_integer_param(params[:page]) || 1
  60. per_page = safe_integer_param(params[:per_page]) || 20
  61. # 筛选参数
  62. status = params[:status]
  63. reason = params[:reason]
  64. user_id = safe_integer_param(params[:user_id])
  65. check_in_id = safe_integer_param(params[:check_in_id])
  66. reports = ContentReport.includes(:user, :check_in, :admin)
  67. .order(created_at: :desc)
  68. # 应用筛选
  69. reports = reports.where(status: status) if status.present?
  70. reports = reports.where(reason: reason) if reason.present?
  71. reports = reports.where(user_id: user_id) if user_id.present?
  72. reports = reports.where(check_in_id: check_in_id) if check_in_id.present?
  73. # 分页
  74. paginated_reports = reports.page(page).per(per_page)
  75. render_success(
  76. data: {
  77. reports: paginated_reports.map { |report| content_report_response_data(report, detailed: true) },
  78. pagination: {
  79. current_page: page,
  80. per_page: per_page,
  81. total_pages: paginated_reports.total_pages,
  82. total_count: paginated_reports.total_count
  83. },
  84. filters: {
  85. status: status,
  86. reason: reason,
  87. user_id: user_id,
  88. check_in_id: check_in_id
  89. }
  90. },
  91. message: '举报列表获取成功'
  92. )
  93. log_api_call('content_reports#index')
  94. rescue => e
  95. render_error(
  96. message: '获取举报列表失败',
  97. errors: [e.message],
  98. code: 'REPORTS_LIST_ERROR'
  99. )
  100. end
  101. # GET /api/v1/content_reports/:id
  102. # 获取举报详情
  103. def show
  104. unless @report.user == current_user || current_user.can_approve_events?
  105. render_error(
  106. message: '权限不足',
  107. code: 'FORBIDDEN',
  108. status: :forbidden
  109. )
  110. return
  111. end
  112. render_success(
  113. data: content_report_response_data(@report, detailed: true),
  114. message: '举报详情获取成功'
  115. )
  116. rescue => e
  117. render_error(
  118. message: '获取举报详情失败',
  119. errors: [e.message],
  120. code: 'REPORT_SHOW_ERROR'
  121. )
  122. end
  123. # PUT /api/v1/content_reports/:id
  124. # 处理举报(管理员)
  125. def update
  126. action = params[:action]
  127. notes = params[:notes]
  128. if action.blank?
  129. render_error(
  130. message: '请选择处理动作',
  131. code: 'MISSING_ACTION',
  132. status: :unprocessable_entity
  133. )
  134. return
  135. end
  136. # 验证处理动作
  137. valid_actions = %w[reviewed dismissed action_taken]
  138. unless valid_actions.include?(action)
  139. render_error(
  140. message: '无效的处理动作',
  141. code: 'INVALID_ACTION',
  142. status: :unprocessable_entity
  143. )
  144. return
  145. end
  146. # 处理举报
  147. result = @report.review!(
  148. admin: current_user,
  149. notes: notes,
  150. action: action.to_sym
  151. )
  152. if result
  153. render_success(
  154. data: content_report_response_data(@report, detailed: true),
  155. message: '举报处理成功'
  156. )
  157. log_api_call('content_reports#update')
  158. else
  159. render_error(
  160. message: '举报处理失败',
  161. code: 'REPORT_UPDATE_FAILED',
  162. status: :unprocessable_entity
  163. )
  164. end
  165. rescue => e
  166. render_error(
  167. message: '处理举报失败',
  168. errors: [e.message],
  169. code: 'REPORT_UPDATE_ERROR'
  170. )
  171. end
  172. # POST /api/v1/content_reports/batch_process
  173. # 批量处理举报(管理员)
  174. def batch_process
  175. report_ids = params[:report_ids]
  176. action = params[:action]
  177. notes = params[:notes]
  178. if report_ids.blank? || !report_ids.is_a?(Array)
  179. render_error(
  180. message: '请提供举报ID列表',
  181. code: 'MISSING_REPORT_IDS',
  182. status: :unprocessable_entity
  183. )
  184. return
  185. end
  186. if action.blank?
  187. render_error(
  188. message: '请选择处理动作',
  189. code: 'MISSING_ACTION',
  190. status: :unprocessable_entity
  191. )
  192. return
  193. end
  194. # 验证处理动作
  195. valid_actions = %w[reviewed dismissed action_taken]
  196. unless valid_actions.include?(action)
  197. render_error(
  198. message: '无效的处理动作',
  199. code: 'INVALID_ACTION',
  200. status: :unprocessable_entity
  201. )
  202. return
  203. end
  204. # 批量处理
  205. result = ContentModerationService.batch_process_reports(
  206. current_user,
  207. report_ids,
  208. action: action.to_sym,
  209. notes: notes
  210. )
  211. if result[:success]
  212. render_success(
  213. data: result,
  214. message: "批量处理完成:成功处理 #{result[:processed_count]}/#{result[:total_count]} 个举报"
  215. )
  216. log_api_call('content_reports#batch_process')
  217. else
  218. render_error(
  219. message: result[:error],
  220. code: 'BATCH_PROCESS_FAILED'
  221. )
  222. end
  223. rescue => e
  224. render_error(
  225. message: '批量处理失败',
  226. errors: [e.message],
  227. code: 'BATCH_PROCESS_ERROR'
  228. )
  229. end
  230. # GET /api/v1/content_reports/statistics
  231. # 获取举报统计(管理员)
  232. def statistics
  233. days = safe_integer_param(params[:days]) || 30
  234. stats = ContentModerationService.get_statistics(days)
  235. render_success(
  236. data: stats,
  237. message: '举报统计获取成功'
  238. )
  239. log_api_call('content_reports#statistics')
  240. rescue => e
  241. render_error(
  242. message: '获取举报统计失败',
  243. errors: [e.message],
  244. code: 'STATISTICS_ERROR'
  245. )
  246. end
  247. # GET /api/v1/content_reports/pending
  248. # 获取待处理举报(管理员)
  249. def pending
  250. limit = safe_integer_param(params[:limit]) || 50
  251. reports = ContentModerationService.get_pending_reports(limit: limit)
  252. render_success(
  253. data: {
  254. reports: reports.map { |report| content_report_response_data(report, detailed: true) },
  255. count: reports.count,
  256. limit: limit
  257. },
  258. message: '待处理举报列表获取成功'
  259. )
  260. log_api_call('content_reports#pending')
  261. rescue => e
  262. render_error(
  263. message: '获取待处理举报失败',
  264. errors: [e.message],
  265. code: 'PENDING_REPORTS_ERROR'
  266. )
  267. end
  268. # GET /api/v1/content_reports/high_priority
  269. # 获取高优先级举报(管理员)
  270. def high_priority
  271. reports = ContentModerationService.get_high_priority_reports
  272. render_success(
  273. data: {
  274. reports: reports.map { |report| content_report_response_data(report, detailed: true) },
  275. count: reports.count
  276. },
  277. message: '高优先级举报列表获取成功'
  278. )
  279. log_api_call('content_reports#high_priority')
  280. rescue => e
  281. render_error(
  282. message: '获取高优先级举报失败',
  283. errors: [e.message],
  284. code: 'HIGH_PRIORITY_REPORTS_ERROR'
  285. )
  286. end
  287. # GET /api/v1/content_reports/export
  288. # 导出举报数据(管理员)
  289. def export
  290. start_date = parse_date_param(params[:start_date]) || 30.days.ago.to_date
  291. end_date = parse_date_param(params[:end_date]) || Date.current
  292. report = ContentModerationService.generate_moderation_report(start_date, end_date)
  293. render_success(
  294. data: report,
  295. message: '举报报告生成成功'
  296. )
  297. log_api_call('content_reports#export')
  298. rescue => e
  299. render_error(
  300. message: '导出举报数据失败',
  301. errors: [e.message],
  302. code: 'EXPORT_ERROR'
  303. )
  304. end
  305. # GET /api/v1/content_reports/my_reports
  306. # 获取我的举报历史
  307. def my_reports
  308. page = safe_integer_param(params[:page]) || 1
  309. per_page = safe_integer_param(params[:per_page]) || 20
  310. reports = current_user.content_reports
  311. .includes(:check_in, :admin)
  312. .order(created_at: :desc)
  313. .page(page)
  314. .per(per_page)
  315. render_success(
  316. data: {
  317. reports: reports.map { |report| content_report_response_data(report, detailed: true) },
  318. pagination: {
  319. current_page: page,
  320. per_page: per_page,
  321. total_pages: reports.total_pages,
  322. total_count: reports.total_count
  323. }
  324. },
  325. message: '我的举报历史获取成功'
  326. )
  327. log_api_call('content_reports#my_reports')
  328. rescue => e
  329. render_error(
  330. message: '获取举报历史失败',
  331. errors: [e.message],
  332. code: 'MY_REPORTS_ERROR'
  333. )
  334. end
  335. private
  336. def set_check_in
  337. check_in_id = safe_integer_param(params[:check_in_id])
  338. unless check_in_id
  339. render_error(
  340. message: '打卡ID不能为空',
  341. code: 'MISSING_CHECK_IN_ID',
  342. status: :unprocessable_entity
  343. )
  344. return
  345. end
  346. @check_in = CheckIn.find_by(id: check_in_id)
  347. unless @check_in
  348. render_error(
  349. message: '打卡不存在',
  350. code: 'CHECK_IN_NOT_FOUND',
  351. status: :not_found
  352. )
  353. return
  354. end
  355. rescue ActiveRecord::RecordNotFound
  356. render_error(
  357. message: '打卡不存在',
  358. code: 'CHECK_IN_NOT_FOUND',
  359. status: :not_found
  360. )
  361. end
  362. def set_report
  363. @report = ContentReport.find(params[:id])
  364. rescue ActiveRecord::RecordNotFound
  365. render_error(
  366. message: '举报记录不存在',
  367. code: 'REPORT_NOT_FOUND',
  368. status: :not_found
  369. )
  370. end
  371. def check_admin_permissions
  372. unless current_user.can_approve_events?
  373. render_error(
  374. message: '权限不足',
  375. code: 'FORBIDDEN',
  376. status: :forbidden
  377. )
  378. end
  379. end
  380. def content_report_response_data(report, detailed: false)
  381. base_data = {
  382. id: report.id,
  383. reason: report.reason,
  384. reason_text: report.reason_text,
  385. description: report.description,
  386. status: report.status,
  387. status_text: report.status_text,
  388. created_at: report.created_at,
  389. updated_at: report.updated_at
  390. }
  391. if detailed
  392. base_data[:user] = {
  393. id: report.user.id,
  394. nickname: report.user.nickname,
  395. avatar_url: report.user.avatar_url
  396. }
  397. base_data[:check_in] = {
  398. id: report.check_in.id,
  399. content: report.check_in.content_preview(200),
  400. created_at: report.check_in.created_at,
  401. user: {
  402. id: report.check_in.user.id,
  403. nickname: report.check_in.user.nickname
  404. }
  405. }
  406. base_data[:admin] = report.admin ? {
  407. id: report.admin.id,
  408. nickname: report.admin.nickname
  409. } : nil
  410. base_data[:admin_notes] = report.admin_notes
  411. base_data[:reviewed_at] = report.reviewed_at
  412. end
  413. base_data
  414. end
  415. # 辅助方法
  416. def safe_integer_param(param)
  417. return nil if param.blank?
  418. Integer(param)
  419. rescue ArgumentError, TypeError
  420. nil
  421. end
  422. def parse_date_param(param)
  423. return nil if param.blank?
  424. Date.parse(param.to_s)
  425. rescue ArgumentError, TypeError
  426. nil
  427. end
  428. end

app/controllers/api/v1/content_search_controller.rb

0.0% lines covered

269 relevant lines. 0 lines covered and 269 lines missed.
    
  1. class Api::V1::ContentSearchController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. # GET /api/v1/content_search
  4. # 内容搜索
  5. def index
  6. search_params = build_search_params
  7. # 执行搜索
  8. result = ContentSearchService.search(search_params)
  9. render_success(
  10. data: result.to_h,
  11. message: '搜索完成'
  12. )
  13. log_api_call('content_search#index')
  14. rescue => e
  15. render_error(
  16. message: '搜索失败',
  17. errors: [e.message],
  18. code: 'SEARCH_ERROR'
  19. )
  20. end
  21. # GET /api/v1/content_search/advanced
  22. # 高级搜索
  23. def advanced
  24. search_params = build_search_params
  25. result = ContentSearchService.advanced_search(search_params)
  26. render_success(
  27. data: {
  28. check_ins: result[:check_ins].map(&:to_search_result_h),
  29. options: result[:options]
  30. },
  31. message: '高级搜索完成'
  32. )
  33. log_api_call('content_search#advanced')
  34. rescue => e
  35. render_error(
  36. message: '高级搜索失败',
  37. errors: [e.message],
  38. code: 'ADVANCED_SEARCH_ERROR'
  39. )
  40. end
  41. # GET /api/v1/content_search/suggestions
  42. # 搜索建议
  43. def suggestions
  44. query = params[:q]&.strip
  45. if query.blank?
  46. render_error(
  47. message: '请输入搜索关键词',
  48. code: 'EMPTY_QUERY',
  49. status: :unprocessable_entity
  50. )
  51. return
  52. end
  53. suggestions = generate_search_suggestions(query)
  54. render_success(
  55. data: {
  56. query: query,
  57. suggestions: suggestions
  58. },
  59. message: '搜索建议生成成功'
  60. )
  61. rescue => e
  62. render_error(
  63. message: '生成搜索建议失败',
  64. errors: [e.message],
  65. code: 'SUGGESTIONS_ERROR'
  66. )
  67. end
  68. # GET /api/v1/content_search/popular_keywords
  69. # 热门关键词
  70. def popular_keywords
  71. days = safe_integer_param(params[:days]) || 30
  72. limit = safe_integer_param(params[:limit]) || 20
  73. keywords = ContentSearchService.popular_keywords(limit, days)
  74. render_success(
  75. data: {
  76. keywords: keywords,
  77. period: "#{days}天",
  78. updated_at: Time.current
  79. },
  80. message: '热门关键词获取成功'
  81. )
  82. rescue => e
  83. render_error(
  84. message: '获取热门关键词失败',
  85. errors: [e.message],
  86. code: 'POPULAR_KEYWORDS_ERROR'
  87. )
  88. end
  89. # GET /api/v1/content_search/trends
  90. # 搜索趋势
  91. def trends
  92. days = safe_integer_param(params[:days]) || 7
  93. trends = ContentSearchService.search_trends(days)
  94. render_success(
  95. data: {
  96. trends: trends,
  97. period: "#{days}天"
  98. },
  99. message: '搜索趋势获取成功'
  100. )
  101. rescue => e
  102. render_error(
  103. message: '获取搜索趋势失败',
  104. errors: [e.message],
  105. code: 'TRENDS_ERROR'
  106. )
  107. end
  108. # GET /api/v1/content_search/related/:check_in_id
  109. # 相关内容推荐
  110. def related
  111. check_in_id = safe_integer_param(params[:check_in_id])
  112. unless check_in_id
  113. render_error(
  114. message: '打卡ID不能为空',
  115. code: 'INVALID_CHECK_IN_ID',
  116. status: :unprocessable_entity
  117. )
  118. return
  119. end
  120. check_in = CheckIn.find_by(id: check_in_id)
  121. unless check_in
  122. render_error(
  123. message: '打卡记录不存在',
  124. code: 'CHECK_IN_NOT_FOUND',
  125. status: :not_found
  126. )
  127. return
  128. end
  129. # 检查权限(只有活动参与者可以查看相关内容)
  130. unless current_user.enrolled?(check_in.reading_event) ||
  131. check_in.reading_event.leader == current_user ||
  132. current_user.can_approve_events?
  133. render_error(
  134. message: '权限不足',
  135. code: 'FORBIDDEN',
  136. status: :forbidden
  137. )
  138. return
  139. end
  140. limit = safe_integer_param(params[:limit]) || 5
  141. related_check_ins = ContentSearchService.recommend_related(check_in, limit)
  142. render_success(
  143. data: {
  144. original_check_in: check_in.to_search_result_h,
  145. related_check_ins: related_check_ins.map(&:to_search_result_h),
  146. limit: limit
  147. },
  148. message: '相关内容推荐成功'
  149. )
  150. log_api_call('content_search#related')
  151. rescue ActiveRecord::RecordNotFound
  152. render_error(
  153. message: '打卡记录不存在',
  154. code: 'CHECK_IN_NOT_FOUND',
  155. status: :not_found
  156. )
  157. rescue => e
  158. render_error(
  159. message: '获取相关内容失败',
  160. errors: [e.message],
  161. code: 'RELATED_CONTENT_ERROR'
  162. )
  163. end
  164. # GET /api/v1/content_search/facets
  165. # 搜索统计
  166. def facets
  167. search_params = build_search_params.reject { |_, v| v.blank? }
  168. if search_params.empty?
  169. render_error(
  170. message: '请提供搜索条件',
  171. code: 'EMPTY_SEARCH_PARAMS',
  172. status: :unprocessable_entity
  173. )
  174. return
  175. end
  176. result = ContentSearchService.search(search_params)
  177. render_success(
  178. data: {
  179. facets: result.facets,
  180. total_count: result.total_count,
  181. search_params: search_params
  182. },
  183. message: '搜索统计获取成功'
  184. )
  185. rescue => e
  186. render_error(
  187. message: '获取搜索统计失败',
  188. errors: [e.message],
  189. code: 'FACETS_ERROR'
  190. )
  191. end
  192. # POST /api/v1/content_search/save_search
  193. # 保存搜索历史
  194. def save_search
  195. query = params[:query]&.strip
  196. search_type = params[:search_type] || 'basic'
  197. if query.blank?
  198. render_error(
  199. message: '搜索内容不能为空',
  200. code: 'EMPTY_QUERY',
  201. status: :unprocessable_entity
  202. )
  203. return
  204. end
  205. # 这里可以实现搜索历史保存逻辑
  206. # 例如:保存到用户的搜索历史记录中
  207. render_success(
  208. message: '搜索历史保存成功'
  209. )
  210. log_api_call('content_search#save_search')
  211. rescue => e
  212. render_error(
  213. message: '保存搜索历史失败',
  214. errors: [e.message],
  215. code: 'SAVE_SEARCH_ERROR'
  216. )
  217. end
  218. # GET /api/v1/content_search/history
  219. # 搜索历史
  220. def history
  221. limit = safe_integer_param(params[:limit]) || 10
  222. # 这里可以实现获取用户搜索历史的逻辑
  223. # 暂时返回空数组
  224. history_items = []
  225. render_success(
  226. data: {
  227. history: history_items,
  228. limit: limit
  229. },
  230. message: '搜索历史获取成功'
  231. )
  232. rescue => e
  233. render_error(
  234. message: '获取搜索历史失败',
  235. errors: [e.message],
  236. code: 'SEARCH_HISTORY_ERROR'
  237. )
  238. end
  239. private
  240. # 构建搜索参数
  241. def build_search_params
  242. params.permit(
  243. :query, :event_id, :user_id, :date_from, :date_to, :status,
  244. :quality_min, :quality_max, :keywords, :sort_by, :sort_direction,
  245. :page, :per_page
  246. ).to_h
  247. end
  248. # 生成搜索建议
  249. def generate_search_suggestions(query)
  250. suggestions = []
  251. # 拼写检查建议
  252. suggestions.concat(spell_check_suggestions(query))
  253. # 热门关键词建议
  254. popular_keywords = ContentSearchService.popular_keywords(10, 7)
  255. matching_keywords = popular_keywords.keys.select { |keyword| keyword.include?(query) }
  256. suggestions.concat(matching_keywords.map { |keyword| "#{keyword} (#{popular_keywords[keyword]}次)" })
  257. # 相关搜索建议
  258. suggestions.concat(related_search_suggestions(query))
  259. suggestions.uniq.first(10)
  260. end
  261. # 拼写检查建议
  262. def spell_check_suggestions(query)
  263. # 简化的拼写检查实现
  264. # 实际应用中可以使用更复杂的算法
  265. suggestions = []
  266. popular_keywords = ContentSearchService.popular_keywords(50, 30)
  267. # 简单的编辑距离计算
  268. popular_keywords.keys.each do |keyword|
  269. distance = levenshtein_distance(query.downcase, keyword.downcase)
  270. if distance <= 2 && distance > 0
  271. suggestions << keyword
  272. end
  273. end
  274. suggestions.first(5)
  275. end
  276. # 相关搜索建议
  277. def related_search_suggestions(query)
  278. # 基于用户历史和热门搜索生成相关建议
  279. suggestions = []
  280. # 可以添加更多相关搜索逻辑
  281. suggestions
  282. end
  283. # 计算编辑距离(简化版)
  284. def levenshtein_distance(str1, str2)
  285. matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
  286. (0..str1.length).each { |i| matrix[i][0] = i }
  287. (0..str2.length).each { |j| matrix[0][j] = j }
  288. (1..str1.length).each do |i|
  289. (1..str2.length).each do |j|
  290. cost = str1[i-1] == str2[j-1] ? 0 : 1
  291. matrix[i][j] = [
  292. matrix[i-1][j] + 1, # deletion
  293. matrix[i][j-1] + 1, # insertion
  294. matrix[i-1][j-1] + cost # substitution
  295. ].min
  296. end
  297. end
  298. matrix[str1.length][str2.length]
  299. end
  300. # 辅助方法
  301. def safe_integer_param(param)
  302. return nil if param.blank?
  303. Integer(param)
  304. rescue ArgumentError, TypeError
  305. nil
  306. end
  307. end

app/controllers/api/v1/daily_leadings_controller.rb

0.0% lines covered

224 relevant lines. 0 lines covered and 224 lines missed.
    
  1. class Api::V1::DailyLeadingsController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :set_reading_event
  4. before_action :set_reading_schedule
  5. before_action :set_daily_leading, only: [:show, :update, :destroy]
  6. # POST /api/v1/reading_schedules/:reading_schedule_id/daily_leading
  7. # 创建领读内容
  8. def create
  9. # 检查权限:领读人(权限窗口内)或活动创建者
  10. unless can_create_daily_leading?
  11. render_error(
  12. message: '权限不足,只能在指定时间窗口内发布领读内容',
  13. code: 'FORBIDDEN',
  14. status: :forbidden
  15. )
  16. return
  17. end
  18. return unless validate_required_fields(:content)
  19. ActiveRecord::Base.transaction do
  20. daily_leading = @reading_schedule.build_daily_leading(
  21. reading_suggestion: params[:reading_suggestion] || params[:content],
  22. questions: params[:questions] || "暂无问题",
  23. leader: current_user
  24. )
  25. if daily_leading.save
  26. # 通知领读内容已发布 (暂时注释掉,因为服务尚未实现)
  27. # @reading_schedule.notify_leading_content_published
  28. leading_data = build_daily_leading_data(daily_leading)
  29. render_success(
  30. data: leading_data,
  31. message: '领读内容发布成功'
  32. )
  33. log_api_call('daily_leadings#create')
  34. else
  35. render_error(
  36. message: '领读内容发布失败',
  37. errors: daily_leading.errors.full_messages,
  38. code: 'VALIDATION_ERROR'
  39. )
  40. end
  41. end
  42. rescue => e
  43. render_error(
  44. message: '领读内容发布失败',
  45. errors: [e.message],
  46. code: 'DAILY_LEADING_ERROR'
  47. )
  48. end
  49. # GET /api/v1/reading_schedules/:reading_schedule_id/daily_leading
  50. # 获取领读内容
  51. def show
  52. unless can_view_daily_leading?
  53. render_error(
  54. message: '权限不足',
  55. code: 'FORBIDDEN',
  56. status: :forbidden
  57. )
  58. return
  59. end
  60. if @daily_leading
  61. leading_data = build_daily_leading_data(@daily_leading, detailed: true)
  62. render_success(data: leading_data)
  63. else
  64. render_success(
  65. data: nil,
  66. message: '暂无领读内容'
  67. )
  68. end
  69. log_api_call('daily_leadings#show')
  70. end
  71. # PUT/PATCH /api/v1/reading_schedules/:reading_schedule_id/daily_leading
  72. # 更新领读内容
  73. def update
  74. unless @daily_leading
  75. render_error(
  76. message: '领读内容不存在',
  77. code: 'DAILY_LEADING_NOT_FOUND',
  78. status: :not_found
  79. )
  80. return
  81. end
  82. # 检查权限:领读人(权限窗口内)或活动创建者
  83. unless can_update_daily_leading?
  84. render_error(
  85. message: '权限不足,只能在指定时间窗口内更新领读内容',
  86. code: 'FORBIDDEN',
  87. status: :forbidden
  88. )
  89. return
  90. end
  91. update_params = {}
  92. if params[:content].present? || params[:reading_suggestion].present?
  93. update_params[:reading_suggestion] = params[:reading_suggestion] || params[:content]
  94. end
  95. if params[:questions].present?
  96. update_params[:questions] = params[:questions]
  97. end
  98. if update_params.empty?
  99. render_error(
  100. message: '没有可更新的字段',
  101. code: 'NO_UPDATABLE_FIELDS'
  102. )
  103. return
  104. end
  105. ActiveRecord::Base.transaction do
  106. if @daily_leading.update(update_params)
  107. leading_data = build_daily_leading_data(@daily_leading, detailed: true)
  108. render_success(
  109. data: leading_data,
  110. message: '领读内容更新成功'
  111. )
  112. log_api_call('daily_leadings#update')
  113. else
  114. render_error(
  115. message: '领读内容更新失败',
  116. errors: @daily_leading.errors.full_messages,
  117. code: 'VALIDATION_ERROR'
  118. )
  119. end
  120. end
  121. rescue => e
  122. render_error(
  123. message: '领读内容更新失败',
  124. errors: [e.message],
  125. code: 'DAILY_LEADING_UPDATE_ERROR'
  126. )
  127. end
  128. # DELETE /api/v1/reading_schedules/:reading_schedule_id/daily_leading
  129. # 删除领读内容
  130. def destroy
  131. unless @daily_leading
  132. render_error(
  133. message: '领读内容不存在',
  134. code: 'DAILY_LEADING_NOT_FOUND',
  135. status: :not_found
  136. )
  137. return
  138. end
  139. # 检查权限:只有活动创建者可以删除领读内容
  140. unless @reading_event.leader == current_user
  141. render_error(
  142. message: '只有活动创建者可以删除领读内容',
  143. code: 'FORBIDDEN',
  144. status: :forbidden
  145. )
  146. return
  147. end
  148. ActiveRecord::Base.transaction do
  149. @daily_leading.destroy!
  150. render_success(message: '领读内容删除成功')
  151. log_api_call('daily_leadings#destroy')
  152. end
  153. rescue ActiveRecord::RecordNotDestroyed
  154. render_error(
  155. message: '领读内容删除失败',
  156. code: 'DELETE_FAILED'
  157. )
  158. end
  159. private
  160. def set_reading_event
  161. event_id = params[:reading_event_id]
  162. @reading_event = ReadingEvent.find(event_id)
  163. rescue ActiveRecord::RecordNotFound
  164. render_error(
  165. message: '活动不存在',
  166. code: 'EVENT_NOT_FOUND',
  167. status: :not_found
  168. )
  169. end
  170. def set_reading_schedule
  171. schedule_id = params[:reading_schedule_id]
  172. @reading_schedule = @reading_event.reading_schedules.find(schedule_id)
  173. rescue ActiveRecord::RecordNotFound
  174. render_error(
  175. message: '阅读计划不存在',
  176. code: 'SCHEDULE_NOT_FOUND',
  177. status: :not_found
  178. )
  179. end
  180. def set_daily_leading
  181. @daily_leading = @reading_schedule.daily_leading
  182. rescue ActiveRecord::RecordNotFound
  183. @daily_leading = nil
  184. end
  185. def build_daily_leading_data(daily_leading, detailed: false)
  186. data = {
  187. id: daily_leading.id,
  188. reading_suggestion: daily_leading.reading_suggestion,
  189. questions: daily_leading.questions,
  190. created_at: daily_leading.created_at,
  191. updated_at: daily_leading.updated_at
  192. }
  193. if detailed
  194. data[:reading_schedule] = {
  195. id: daily_leading.reading_schedule.id,
  196. day_number: daily_leading.reading_schedule.day_number,
  197. date: daily_leading.reading_schedule.date
  198. }
  199. data[:reading_event] = {
  200. id: daily_leading.reading_schedule.reading_event.id,
  201. title: daily_leading.reading_schedule.reading_event.title,
  202. book_name: daily_leading.reading_schedule.reading_event.book_name
  203. }
  204. data[:leader] = daily_leading.leader ? {
  205. id: daily_leading.leader.id,
  206. nickname: daily_leading.leader.nickname
  207. } : nil
  208. data[:permissions] = {
  209. can_view: can_view_daily_leading?,
  210. can_update: can_update_daily_leading?,
  211. can_delete: can_delete_daily_leading?
  212. }
  213. end
  214. data
  215. end
  216. # 权限检查方法
  217. def can_create_daily_leading?
  218. # 活动创建者始终可以创建
  219. return true if @reading_event.leader == current_user
  220. # 领读人在权限窗口内可以创建
  221. return true if @reading_schedule.daily_leader == current_user &&
  222. @reading_schedule.can_publish_leading_content?
  223. false
  224. end
  225. def can_view_daily_leading?
  226. # 活动创建者、领读人、参与者都可以查看
  227. return true if @reading_event.leader == current_user
  228. return true if @reading_schedule.daily_leader == current_user
  229. return true if @reading_event.participants.include?(current_user)
  230. false
  231. end
  232. def can_update_daily_leading?
  233. # 活动创建者始终可以更新
  234. return true if @reading_event.leader == current_user
  235. # 领读人在权限窗口内可以更新
  236. return true if @reading_schedule.daily_leader == current_user &&
  237. @reading_schedule.can_publish_leading_content?
  238. false
  239. end
  240. def can_delete_daily_leading?
  241. # 只有活动创建者可以删除
  242. @reading_event.leader == current_user
  243. end
  244. end

app/controllers/api/v1/event_enrollments_controller.rb

0.0% lines covered

264 relevant lines. 0 lines covered and 264 lines missed.
    
  1. class Api::V1::EventEnrollmentsController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :set_reading_event
  4. before_action :set_enrollment, only: [:show, :cancel, :update]
  5. # POST /api/v1/event_enrollments
  6. # 报名参加活动
  7. def create
  8. return unless validate_required_fields(:reading_event_id)
  9. ActiveRecord::Base.transaction do
  10. # 检查活动是否可以报名
  11. unless @reading_event.can_enroll?
  12. render_error(
  13. message: @reading_event.enrollment_error_message || '活动当前无法报名',
  14. code: 'CANNOT_ENROLL',
  15. status: :unprocessable_entity
  16. )
  17. return
  18. end
  19. # 检查用户是否已经报名
  20. existing_enrollment = @reading_event.event_enrollments.find_by(user: current_user)
  21. if existing_enrollment
  22. render_error(
  23. message: '您已经报名过此活动',
  24. code: 'ALREADY_ENROLLED',
  25. status: :unprocessable_entity
  26. )
  27. return
  28. end
  29. # 创建报名记录
  30. enrollment = @reading_event.event_enrollments.build(
  31. user: current_user,
  32. enrollment_type: params[:enrollment_type]&.to_s || 'participant',
  33. status: 'enrolled',
  34. enrollment_date: Time.current
  35. )
  36. # 处理费用(如果有)
  37. if @reading_event.fee_type != 'free'
  38. fee_amount = @reading_event.fee_amount
  39. enrollment.fee_paid_amount = fee_amount
  40. # 这里应该调用支付服务
  41. # payment_result = PaymentService.process(current_user, fee_amount, @reading_event)
  42. # unless payment_result.success?
  43. # render_error(message: '支付失败', code: 'PAYMENT_FAILED', status: :unprocessable_entity)
  44. # return
  45. # end
  46. end
  47. if enrollment.save
  48. # 发送报名确认通知(暂时注释掉,因为服务未实现)
  49. # enrollment.notify_enrollment_confirmation
  50. enrollment_data = build_enrollment_data(enrollment)
  51. render_success(
  52. data: enrollment_data,
  53. message: '报名成功'
  54. )
  55. log_api_call('event_enrollments#create')
  56. else
  57. render_error(
  58. message: '报名失败',
  59. errors: enrollment.errors.full_messages,
  60. code: 'VALIDATION_ERROR'
  61. )
  62. end
  63. end
  64. rescue => e
  65. render_error(
  66. message: '报名处理失败',
  67. errors: [e.message],
  68. code: 'ENROLLMENT_ERROR'
  69. )
  70. end
  71. # GET /api/v1/event_enrollments/:id
  72. # 获取报名详情
  73. def show
  74. unless @enrollment
  75. render_error(
  76. message: '报名记录不存在',
  77. code: 'ENROLLMENT_NOT_FOUND',
  78. status: :not_found
  79. )
  80. return
  81. end
  82. # 检查权限:只有报名者本人或活动创建者可以查看
  83. unless @enrollment.user == current_user || @reading_event.leader == current_user
  84. render_error(
  85. message: '权限不足',
  86. code: 'FORBIDDEN',
  87. status: :forbidden
  88. )
  89. return
  90. end
  91. enrollment_data = build_enrollment_data(@enrollment, detailed: true)
  92. render_success(data: enrollment_data)
  93. log_api_call('event_enrollments#show')
  94. end
  95. # POST /api/v1/event_enrollments/:id/cancel
  96. # 取消报名
  97. def cancel
  98. unless @enrollment
  99. render_error(
  100. message: '报名记录不存在',
  101. code: 'ENROLLMENT_NOT_FOUND',
  102. status: :not_found
  103. )
  104. return
  105. end
  106. # 检查权限:只有报名者本人可以取消
  107. unless @enrollment.user == current_user
  108. render_error(
  109. message: '权限不足',
  110. code: 'FORBIDDEN',
  111. status: :forbidden
  112. )
  113. return
  114. end
  115. # 检查是否可以取消
  116. unless @enrollment.can_cancel?
  117. render_error(
  118. message: @enrollment.cancellation_error_message || '当前状态无法取消报名',
  119. code: 'CANNOT_CANCEL',
  120. status: :unprocessable_entity
  121. )
  122. return
  123. end
  124. ActiveRecord::Base.transaction do
  125. # 处理退款(如果有)
  126. if @enrollment.fee_paid_amount > 0
  127. @enrollment.process_refund!
  128. end
  129. # 更新状态
  130. @enrollment.update!(status: 'cancelled')
  131. # 发送取消通知
  132. # EnrollmentNotificationService.notify_cancellation(@enrollment)
  133. render_success(
  134. data: build_enrollment_data(@enrollment),
  135. message: '报名已取消'
  136. )
  137. log_api_call('event_enrollments#cancel')
  138. end
  139. rescue => e
  140. render_error(
  141. message: '取消报名失败',
  142. errors: [e.message],
  143. code: 'CANCELLATION_ERROR'
  144. )
  145. end
  146. # PUT/PATCH /api/v1/event_enrollments/:id
  147. # 更新报名信息(仅限特定字段)
  148. def update
  149. unless @enrollment
  150. render_error(
  151. message: '报名记录不存在',
  152. code: 'ENROLLMENT_NOT_FOUND',
  153. status: :not_found
  154. )
  155. return
  156. end
  157. # 检查权限:只有报名者本人可以更新
  158. unless @enrollment.user == current_user
  159. render_error(
  160. message: '权限不足',
  161. code: 'FORBIDDEN',
  162. status: :forbidden
  163. )
  164. return
  165. end
  166. # 只允许更新特定字段
  167. allowed_fields = [:enrollment_type]
  168. update_params = params.slice(*allowed_fields).compact
  169. if update_params.empty?
  170. render_error(
  171. message: '没有可更新的字段',
  172. code: 'NO_UPDATABLE_FIELDS'
  173. )
  174. return
  175. end
  176. ActiveRecord::Base.transaction do
  177. if @enrollment.update(update_params)
  178. render_success(
  179. data: build_enrollment_data(@enrollment),
  180. message: '报名信息更新成功'
  181. )
  182. log_api_call('event_enrollments#update')
  183. else
  184. render_error(
  185. message: '更新失败',
  186. errors: @enrollment.errors.full_messages,
  187. code: 'VALIDATION_ERROR'
  188. )
  189. end
  190. end
  191. rescue => e
  192. render_error(
  193. message: '更新失败',
  194. errors: [e.message],
  195. code: 'UPDATE_ERROR'
  196. )
  197. end
  198. # GET /api/v1/reading_events/:reading_event_id/enrollments
  199. # 获取活动的报名列表(活动创建者可用)
  200. def index
  201. # 检查权限:只有活动创建者可以查看报名列表
  202. unless @reading_event.leader == current_user
  203. render_error(
  204. message: '权限不足',
  205. code: 'FORBIDDEN',
  206. status: :forbidden
  207. )
  208. return
  209. end
  210. # 获取报名列表
  211. enrollments = @reading_event.event_enrollments
  212. .includes(:user)
  213. .order(enrollment_date: :desc)
  214. # 分页
  215. pagination = pagination_params
  216. enrollments = enrollments.page(pagination[:page]).per(pagination[:per_page])
  217. # 构建响应数据
  218. enrollments_data = enrollments.map do |enrollment|
  219. build_enrollment_data(enrollment, detailed: true)
  220. end
  221. render_success(
  222. data: enrollments_data,
  223. meta: pagination_meta(enrollments)
  224. )
  225. log_api_call('event_enrollments#index')
  226. end
  227. # GET /api/v1/reading_events/:reading_event_id/enrollments/statistics
  228. # 获取活动报名统计(活动创建者可用)
  229. def statistics
  230. # 检查权限:只有活动创建者可以查看统计
  231. unless @reading_event.leader == current_user
  232. render_error(
  233. message: '权限不足',
  234. code: 'FORBIDDEN',
  235. status: :forbidden
  236. )
  237. return
  238. end
  239. # 计算统计数据
  240. stats = @reading_event.event_enrollments.calculate_enrollment_statistics
  241. render_success(data: stats)
  242. log_api_call('event_enrollments#statistics')
  243. end
  244. private
  245. def set_reading_event
  246. event_id = params[:reading_event_id] || params[:id]
  247. @reading_event = ReadingEvent.find(event_id)
  248. rescue ActiveRecord::RecordNotFound
  249. render_error(
  250. message: '活动不存在',
  251. code: 'EVENT_NOT_FOUND',
  252. status: :not_found
  253. )
  254. end
  255. def set_enrollment
  256. @enrollment = @reading_event.event_enrollments.find(params[:id])
  257. rescue ActiveRecord::RecordNotFound
  258. @enrollment = nil
  259. end
  260. def build_enrollment_data(enrollment, detailed: false)
  261. data = {
  262. id: enrollment.id,
  263. enrollment_type: enrollment.enrollment_type,
  264. status: enrollment.status,
  265. enrollment_date: enrollment.enrollment_date,
  266. completion_rate: enrollment.completion_rate,
  267. check_ins_count: enrollment.check_ins_count,
  268. leader_days_count: enrollment.leader_days_count,
  269. flowers_received_count: enrollment.flowers_received_count,
  270. fee_paid_amount: enrollment.fee_paid_amount,
  271. fee_refund_amount: enrollment.fee_refund_amount,
  272. refund_status: enrollment.refund_status,
  273. created_at: enrollment.created_at,
  274. updated_at: enrollment.updated_at
  275. }
  276. if detailed
  277. data[:user] = {
  278. id: enrollment.user.id,
  279. nickname: enrollment.user.nickname,
  280. avatar_url: enrollment.user.avatar_url
  281. }
  282. data[:reading_event] = {
  283. id: enrollment.reading_event.id,
  284. title: enrollment.reading_event.title,
  285. book_name: enrollment.reading_event.book_name
  286. }
  287. data[:permissions] = {
  288. can_cancel: enrollment.can_cancel?,
  289. can_update: enrollment.user == current_user,
  290. can_check_in: enrollment.can_check_in?,
  291. can_receive_flowers: enrollment.can_receive_flowers?,
  292. can_give_flowers: enrollment.can_give_flowers?
  293. }
  294. data[:status_info] = {
  295. can_participate: enrollment.can_participate?,
  296. is_completed: enrollment.is_completed?,
  297. eligible_for_completion_certificate: enrollment.eligible_for_completion_certificate?,
  298. eligible_for_flower_certificate: enrollment.eligible_for_flower_certificate?
  299. }
  300. end
  301. data
  302. end
  303. end

app/controllers/api/v1/flower_comments_controller.rb

0.0% lines covered

53 relevant lines. 0 lines covered and 53 lines missed.
    
  1. # frozen_string_literal: true
  2. class Api::V1::FlowerCommentsController < ApplicationController
  3. before_action :authenticate_user!
  4. before_action :set_flower
  5. # POST /api/flowers/:flower_id/comments
  6. def create
  7. result = FlowerCommentService.add_comment_to_flower(@flower, current_user, comment_params[:content])
  8. if result[:success]
  9. render json: result, status: :created
  10. else
  11. render json: { error: result[:error] }, status: :unprocessable_entity
  12. end
  13. end
  14. # GET /api/flowers/:flower_id/comments
  15. def index
  16. page = params[:page] || 1
  17. limit = params[:limit] || 10
  18. result = FlowerCommentService.get_flower_comments(@flower, page, limit, current_user)
  19. render json: result
  20. end
  21. # GET /api/flowers/:flower_id/comments/stats
  22. def stats
  23. result = FlowerCommentService.get_flower_comment_stats(@flower)
  24. render json: result
  25. end
  26. # DELETE /api/flowers/:flower_id/comments/:id
  27. def destroy
  28. comment = @flower.comments.find(params[:id])
  29. result = FlowerCommentService.delete_flower_comment(@flower, comment, current_user)
  30. if result[:success]
  31. render json: { message: result[:message] }
  32. else
  33. render json: { error: result[:error] }, status: :forbidden
  34. end
  35. end
  36. # DELETE /api/flowers/:flower_id/comments/batch
  37. def batch_destroy
  38. return render json: { error: '需要管理员权限' }, status: :forbidden unless current_user.any_admin?
  39. comment_ids = params[:comment_ids] || []
  40. result = FlowerCommentService.batch_delete_flower_comments(@flower, comment_ids, current_user)
  41. render json: result
  42. end
  43. # GET /api/flowers/:flower_id/comments/search
  44. def search
  45. keyword = params[:q] || params[:keyword]
  46. page = params[:page] || 1
  47. limit = params[:limit] || 10
  48. result = FlowerCommentService.search_flower_comments(@flower, keyword, page, limit, current_user)
  49. render json: result
  50. end
  51. private
  52. def set_flower
  53. @flower = Flower.find(params[:flower_id])
  54. rescue ActiveRecord::RecordNotFound
  55. render json: { error: '小红花不存在' }, status: :not_found
  56. end
  57. def comment_params
  58. params.require(:comment).permit(:content)
  59. end
  60. end

app/controllers/api/v1/flower_incentives_controller.rb

0.0% lines covered

237 relevant lines. 0 lines covered and 237 lines missed.
    
  1. class Api::V1::FlowerIncentivesController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :find_reading_event
  4. before_action :check_event_participation
  5. # 获取用户在活动中的配额信息
  6. def quota_info
  7. quota_info = FlowerIncentiveService.get_user_quota_info(current_user, @reading_event)
  8. if quota_info[:error]
  9. render json: {
  10. success: false,
  11. error: quota_info[:error]
  12. }, status: :unprocessable_entity
  13. else
  14. render json: {
  15. success: true,
  16. data: quota_info
  17. }
  18. end
  19. end
  20. # 赠送小红花(带配额检查和确认提示)
  21. def give_flower
  22. # 验证参数
  23. recipient_id = params[:recipient_id]
  24. check_in_id = params[:check_in_id]
  25. amount = params[:amount]&.to_i || 1
  26. comment = params[:comment]
  27. flower_type = params[:flower_type] || 'regular'
  28. is_anonymous = params[:is_anonymous] == true
  29. # 验证必要参数
  30. unless recipient_id && check_in_id
  31. return render json: {
  32. success: false,
  33. error: '缺少必要参数:recipient_id 和 check_in_id'
  34. }, status: :bad_request
  35. end
  36. # 查找接收者和打卡记录
  37. recipient = User.find_by(id: recipient_id)
  38. check_in = CheckIn.find_by(id: check_in_id)
  39. unless recipient && check_in
  40. return render json: {
  41. success: false,
  42. error: '接收者或打卡记录不存在'
  43. }, status: :not_found
  44. end
  45. # 验证打卡记录是否属于当前活动
  46. if check_in.reading_schedule&.reading_event_id != @reading_event.id
  47. return render json: {
  48. success: false,
  49. error: '该打卡记录不属于当前活动'
  50. }, status: :unprocessable_entity
  51. end
  52. # 检查是否是给自己赠送
  53. if recipient.id == current_user.id
  54. return render json: {
  55. success: false,
  56. error: '不能给自己赠送小红花'
  57. }, status: :unprocessable_entity
  58. end
  59. # 检查配额
  60. unless FlowerIncentiveService.can_give_flower?(current_user, @reading_event, amount)
  61. return render json: {
  62. success: false,
  63. error: '小红花配额不足',
  64. quota_info: FlowerIncentiveService.get_user_quota_info(current_user, @reading_event)
  65. }, status: :unprocessable_entity
  66. end
  67. # 根据请求类型处理(确认或直接赠送)
  68. if params[:confirm] == true
  69. # 用户已确认,执行赠送
  70. result = FlowerIncentiveService.give_flower_with_quota(
  71. current_user,
  72. recipient,
  73. check_in,
  74. amount: amount,
  75. comment: comment,
  76. flower_type: flower_type,
  77. is_anonymous: is_anonymous
  78. )
  79. if result[:success]
  80. render json: {
  81. success: true,
  82. message: '小红花赠送成功!',
  83. data: {
  84. flower: result[:flower].as_json_for_api,
  85. remaining_quota: result[:remaining_quota],
  86. warning: '赠送成功后无法撤回,请谨慎操作'
  87. }
  88. }
  89. else
  90. render json: {
  91. success: false,
  92. error: result[:error],
  93. message: '小红花赠送失败,请重试'
  94. }, status: :unprocessable_entity
  95. end
  96. else
  97. # 需要用户确认
  98. quota_info = FlowerIncentiveService.get_user_quota_info(current_user, @reading_event)
  99. render json: {
  100. success: true,
  101. require_confirmation: true,
  102. message: '即将赠送小红花,此操作不可撤回,请确认',
  103. data: {
  104. recipient: recipient.as_json_for_api,
  105. check_in: {
  106. id: check_in.id,
  107. content: check_in.content.truncate(100),
  108. user: check_in.user.as_json_for_api
  109. },
  110. amount: amount,
  111. comment: comment,
  112. flower_type: flower_type,
  113. is_anonymous: is_anonymous,
  114. remaining_quota: quota_info[:remaining_flowers],
  115. warning: '赠送成功后无法撤回,请谨慎确认'
  116. }
  117. }
  118. end
  119. end
  120. # 获取活动的前三名排行榜
  121. def top_three
  122. if @reading_event.status != 'completed'
  123. return render json: {
  124. success: false,
  125. error: '活动尚未结束,排行榜暂未生成'
  126. }, status: :unprocessable_entity
  127. end
  128. result = FlowerIncentiveService.get_event_top_three(@reading_event)
  129. if result[:error]
  130. render json: {
  131. success: false,
  132. error: result[:error]
  133. }, status: :unprocessable_entity
  134. else
  135. render json: {
  136. success: true,
  137. data: result
  138. }
  139. end
  140. end
  141. # 获取用户的证书历史
  142. def my_certificates
  143. certificates = FlowerIncentiveService.get_user_certificates(current_user)
  144. render json: {
  145. success: true,
  146. data: certificates
  147. }
  148. end
  149. # 获取证书详情
  150. def certificate_detail
  151. certificate_id = params[:certificate_id]
  152. unless certificate_id
  153. return render json: {
  154. success: false,
  155. error: '缺少证书ID'
  156. }, status: :bad_request
  157. end
  158. certificate = FlowerCertificate.find_by(certificate_id: certificate_id)
  159. unless certificate
  160. return render json: {
  161. success: false,
  162. error: '证书不存在'
  163. }, status: :not_found
  164. end
  165. # 检查权限(只有证书所有者或活动参与者可以查看)
  166. if certificate.user_id != current_user.id && !@reading_event.participants.include?(current_user)
  167. return render json: {
  168. success: false,
  169. error: '没有权限查看该证书'
  170. }, status: :forbidden
  171. end
  172. render json: {
  173. success: true,
  174. data: {
  175. certificate: certificate.as_json_for_api,
  176. event: certificate.reading_event.as_json_for_api,
  177. user: certificate.user.as_json_for_api,
  178. share_url: certificate.share_url,
  179. certificate_image_url: certificate.certificate_image_path
  180. }
  181. }
  182. end
  183. # 生成活动结束证书(管理员权限)
  184. def finalize_certificates
  185. unless current_user.any_admin?
  186. return render json: {
  187. success: false,
  188. error: '没有权限执行此操作'
  189. }, status: :forbidden
  190. end
  191. if @reading_event.status != 'completed'
  192. return render json: {
  193. success: false,
  194. error: '只有已结束的活动才能生成证书'
  195. }, status: :unprocessable_entity
  196. end
  197. result = FlowerIncentiveService.finalize_event_flower_certificates(@reading_event)
  198. if result[:success]
  199. render json: {
  200. success: true,
  201. message: '活动证书生成成功!',
  202. data: result
  203. }
  204. else
  205. render json: {
  206. success: false,
  207. error: result[:error]
  208. }, status: :unprocessable_entity
  209. end
  210. end
  211. # 活动开始时初始化配额(管理员权限)
  212. def initialize_quotas
  213. unless current_user.any_admin?
  214. return render json: {
  215. success: false,
  216. error: '没有权限执行此操作'
  217. }, status: :forbidden
  218. end
  219. max_flowers = params[:max_flowers]&.to_i || 3
  220. if FlowerIncentiveService.initialize_event_flower_quotas(@reading_event, max_flowers: max_flowers)
  221. render json: {
  222. success: true,
  223. message: '活动小红花配额初始化成功',
  224. data: {
  225. event: @reading_event.as_json_for_api,
  226. max_flowers: max_flowers,
  227. participants_count: @reading_event.participants.count
  228. }
  229. }
  230. else
  231. render json: {
  232. success: false,
  233. error: '配额初始化失败,请重试'
  234. }, status: :unprocessable_entity
  235. end
  236. end
  237. private
  238. def find_reading_event
  239. @reading_event = ReadingEvent.find(params[:reading_event_id])
  240. rescue ActiveRecord::RecordNotFound
  241. render json: {
  242. success: false,
  243. error: '活动不存在'
  244. }, status: :not_found
  245. end
  246. def check_event_participation
  247. unless @reading_event.participants.include?(current_user) || current_user.any_admin?
  248. render json: {
  249. success: false,
  250. error: '您尚未参与此活动或没有权限访问'
  251. }, status: :forbidden
  252. end
  253. end
  254. end

app/controllers/api/v1/flower_leaderboards_controller.rb

0.0% lines covered

288 relevant lines. 0 lines covered and 288 lines missed.
    
  1. class Api::V1::FlowerLeaderboardsController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. # GET /api/v1/flower_leaderboards
  4. # 获取小红花排行榜
  5. def index
  6. type = params[:type] || 'received'
  7. period = safe_integer_param(params[:period]) || 30
  8. limit = safe_integer_param(params[:limit]) || 20
  9. # 验证参数
  10. valid_types = %w[received given popular_check_ins generous_givers]
  11. unless valid_types.include?(type)
  12. render_error(
  13. message: '无效的排行榜类型',
  14. code: 'INVALID_TYPE',
  15. status: :unprocessable_entity
  16. )
  17. return
  18. end
  19. if period < 1 || period > 365
  20. render_error(
  21. message: '统计时间范围必须在1-365天之间',
  22. code: 'INVALID_PERIOD',
  23. status: :unprocessable_entity
  24. )
  25. return
  26. end
  27. if limit < 1 || limit > 100
  28. render_error(
  29. message: '显示数量必须在1-100之间',
  30. code: 'INVALID_LIMIT',
  31. status: :unprocessable_entity
  32. )
  33. return
  34. end
  35. # 获取排行榜数据
  36. leaderboard = FlowerStatisticsService.get_flower_leaderboard(type, period, limit)
  37. # 格式化响应数据
  38. formatted_leaderboard = case type.to_sym
  39. when :received
  40. format_user_leaderboard(leaderboard)
  41. when :given
  42. format_user_leaderboard(leaderboard)
  43. when :popular_check_ins
  44. format_check_in_leaderboard(leaderboard)
  45. when :generous_givers
  46. format_user_leaderboard(leaderboard)
  47. else
  48. []
  49. end
  50. render_success(
  51. data: {
  52. leaderboard_type: type,
  53. period: period,
  54. limit: limit,
  55. leaderboard: formatted_leaderboard
  56. },
  57. message: '排行榜获取成功'
  58. )
  59. log_api_call('flower_leaderboards#index')
  60. rescue => e
  61. render_error(
  62. message: '获取排行榜失败',
  63. errors: [e.message],
  64. code: 'LEADERBOARD_ERROR'
  65. )
  66. end
  67. # GET /api/v1/flower_leaderboards/trends
  68. # 获取小红花趋势数据
  69. def trends
  70. days = safe_integer_param(params[:days]) || 30
  71. if days < 1 || days > 90
  72. render_error(
  73. message: '统计时间范围必须在1-90天之间',
  74. code: 'INVALID_PERIOD',
  75. status: :unprocessable_entity
  76. )
  77. return
  78. end
  79. trends = FlowerStatisticsService.get_flower_trends(days)
  80. render_success(
  81. data: {
  82. period: "#{days}天",
  83. trends: trends,
  84. summary: calculate_trends_summary(trends)
  85. },
  86. message: '趋势数据获取成功'
  87. )
  88. log_api_call('flower_leaderboards#trends')
  89. rescue => e
  90. render_error(
  91. message: '获取趋势数据失败',
  92. errors: [e.message],
  93. code: 'TRENDS_ERROR'
  94. )
  95. end
  96. # GET /api/v1/flower_leaderboards/statistics
  97. # 获取小红花统计
  98. def statistics
  99. days = safe_integer_param(params[:days]) || 30
  100. type = params[:type] # 'user' 或 'event'
  101. id = safe_integer_param(params[:id])
  102. if days < 1 || days > 365
  103. render_error(
  104. message: '统计时间范围必须在1-365天之间',
  105. code: 'INVALID_PERIOD',
  106. status: :unprocessable_entity
  107. )
  108. return
  109. end
  110. data = case type
  111. when 'user'
  112. get_user_statistics(id, days)
  113. when 'event'
  114. get_event_statistics(id, days)
  115. when 'incentive'
  116. FlowerStatisticsService.get_incentive_statistics(days)
  117. else
  118. FlowerStatisticsService.get_incentive_statistics(days)
  119. end
  120. if data.nil?
  121. render_error(
  122. message: '无效的统计类型或ID',
  123. code: 'INVALID_TYPE_OR_ID',
  124. status: :unprocessable_entity
  125. )
  126. return
  127. end
  128. render_success(
  129. data: data,
  130. message: '统计数据获取成功'
  131. )
  132. log_api_call('flower_leaderboards#statistics')
  133. rescue => e
  134. render_error(
  135. message: '获取统计数据失败',
  136. errors: [e.message],
  137. code: 'STATISTICS_ERROR'
  138. )
  139. end
  140. # GET /api/v1/flower_leaderboards/suggestions
  141. # 获取小红花发放建议
  142. def suggestions
  143. limit = safe_integer_param(params[:limit]) || 5
  144. if limit < 1 || limit > 20
  145. render_error(
  146. message: '建议数量必须在1-20之间',
  147. code: 'INVALID_LIMIT',
  148. status: :unprocessable_entity
  149. )
  150. return
  151. end
  152. suggestions = FlowerStatisticsService.get_flower_suggestions(current_user, limit)
  153. formatted_suggestions = suggestions.map do |suggestion|
  154. case suggestion[:type]
  155. when :check_in
  156. {
  157. id: suggestion[:check_in].id,
  158. type: 'check_in',
  159. title: suggestion[:check_in].content_preview(100),
  160. author: {
  161. id: suggestion[:check_in].user.id,
  162. nickname: suggestion[:check_in].user.nickname,
  163. avatar_url: suggestion[:check_in].user.avatar_url
  164. },
  165. created_at: suggestion[:check_in].created_at,
  166. flowers_count: suggestion[:check_in].flowers_count,
  167. reason: suggestion[:reason],
  168. priority: suggestion[:priority]
  169. }
  170. when :user
  171. {
  172. id: suggestion[:user].id,
  173. type: 'user',
  174. nickname: suggestion[:user].nickname,
  175. avatar_url: suggestion[:user].avatar_url,
  176. reason: suggestion[:reason],
  177. priority: suggestion[:priority]
  178. }
  179. end
  180. end
  181. render_success(
  182. data: {
  183. suggestions: formatted_suggestions,
  184. limit: limit,
  185. user_id: current_user.id
  186. },
  187. message: '发放建议获取成功'
  188. )
  189. log_api_call('flower_leaderboards#suggestions')
  190. rescue => e
  191. render_error(
  192. message: '获取发放建议失败',
  193. errors: [e.message],
  194. code: 'SUGGESTIONS_ERROR'
  195. )
  196. end
  197. # GET /api/v1/flower_leaderboards/my_ranking
  198. # 获取当前用户的排名
  199. def my_ranking
  200. period = safe_integer_param(params[:period]) || 30
  201. type = params[:type] || 'received'
  202. if period < 1 || period > 365
  203. render_error(
  204. message: '统计时间范围必须在1-365天之间',
  205. code: 'INVALID_PERIOD',
  206. status: :unprocessable_entity
  207. )
  208. return
  209. end
  210. # 获取排行榜
  211. leaderboard = FlowerStatisticsService.get_flower_leaderboard(type, period, 1000)
  212. # 查找当前用户的排名
  213. my_ranking = case type.to_sym
  214. when :received
  215. leaderboard.index { |user| user[:id] == current_user.id }
  216. when :given
  217. leaderboard.index { |user| user[:id] == current_user.id }
  218. else
  219. nil
  220. end
  221. my_stats = FlowerStatisticsService.get_user_flower_stats(current_user, period)
  222. render_success(
  223. data: {
  224. period: period,
  225. type: type,
  226. my_ranking: my_ranking ? my_ranking + 1 : nil, # 排名从1开始
  227. total_users: leaderboard.count,
  228. my_stats: my_stats,
  229. top_10: leaderboard.first(10).map { |user| user[:id] },
  230. percentage: calculate_ranking_percentage(my_ranking, leaderboard.count)
  231. },
  232. message: '个人排名获取成功'
  233. )
  234. log_api_call('flower_leaderboards#my_ranking')
  235. rescue => e
  236. render_error(
  237. message: '获取个人排名失败',
  238. errors: [e.message],
  239. code: 'MY_RANKING_ERROR'
  240. )
  241. end
  242. private
  243. def get_user_statistics(user_id, days)
  244. user = user_id ? User.find_by(id: user_id) : current_user
  245. return nil unless user
  246. FlowerStatisticsService.get_user_flower_stats(user, days)
  247. end
  248. def get_event_statistics(event_id, days)
  249. event = ReadingEvent.find_by(id: event_id)
  250. return nil unless event
  251. FlowerStatisticsService.get_event_flower_stats(event, days)
  252. end
  253. def format_user_leaderboard(leaderboard)
  254. leaderboard.map do |user|
  255. {
  256. id: user.id,
  257. nickname: user.nickname,
  258. avatar_url: user.avatar_url,
  259. total_flowers: user.total_flowers,
  260. rank: leaderboard.index(user) + 1
  261. }
  262. end
  263. end
  264. def format_check_in_leaderboard(leaderboard)
  265. leaderboard.map do |check_in|
  266. {
  267. id: check_in.id,
  268. content: check_in.content_preview(100),
  269. author: {
  270. id: check_in.user.id,
  271. nickname: check_in.user.nickname,
  272. avatar_url: check_in.user.avatar_url
  273. },
  274. created_at: check_in.created_at,
  275. flowers_count: check_in.flower_count,
  276. rank: leaderboard.index(check_in) + 1
  277. }
  278. end
  279. end
  280. def calculate_trends_summary(trends)
  281. total_flowers = trends.values.sum { |day| day[:total] }
  282. avg_flowers = trends.values.sum { |day| day[:total] }.to_f / [trends.count, 1].max
  283. max_flowers = trends.values.map { |day| day[:total] }.max || 0
  284. min_flowers = trends.values.map { |day| day[:total] }.min || 0
  285. {
  286. total_flowers: total_flowers,
  287. avg_flowers: avg_flowers.round(2),
  288. max_flowers: max_flowers,
  289. min_flowers: min_flowers,
  290. trend_days: trends.keys.count
  291. }
  292. end
  293. def calculate_ranking_percentage(rank, total_users)
  294. return 0 if rank.nil? || total_users == 0
  295. ((total_users - rank + 1).to_f / total_users * 100).round(2)
  296. end
  297. # 辅助方法
  298. def safe_integer_param(param)
  299. return nil if param.blank?
  300. Integer(param)
  301. rescue ArgumentError, TypeError
  302. nil
  303. end
  304. end

app/controllers/api/v1/leader_assignments_controller.rb

0.0% lines covered

276 relevant lines. 0 lines covered and 276 lines missed.
    
  1. class Api::V1::LeaderAssignmentsController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :set_reading_event
  4. before_action :check_event_permissions
  5. # POST /api/v1/reading_events/:reading_event_id/leader_assignments/auto_assign
  6. # 自动分配领读人
  7. def auto_assign
  8. assignment_type = params[:assignment_type]&.to_sym || @reading_event.leader_assignment_type.to_sym
  9. unless [:random, :balanced, :rotation, :voluntary].include?(assignment_type)
  10. render_error(
  11. message: '不支持的分配方式',
  12. code: 'UNSUPPORTED_ASSIGNMENT_TYPE',
  13. status: :unprocessable_entity
  14. )
  15. return
  16. end
  17. options = {}
  18. options[:max_leadership_count] = params[:max_leadership_count] if params[:max_leadership_count].present?
  19. options[:volunteer_assignments] = params[:volunteer_assignments] if params[:volunteer_assignments].present?
  20. service = LeaderAssignmentService.auto_assign_leaders!(@reading_event, assignment_type: assignment_type, options: options)
  21. if service.success?
  22. render_success(
  23. data: {
  24. assignment_type: service.result[:assignment_type],
  25. assigned_count: service.result[:assigned_count],
  26. statistics: get_assignment_statistics
  27. },
  28. message: service.result[:message]
  29. )
  30. log_api_call('leader_assignments#auto_assign')
  31. else
  32. render_error(
  33. message: service.error_message,
  34. code: 'AUTO_ASSIGN_FAILED',
  35. status: :unprocessable_entity
  36. )
  37. end
  38. rescue => e
  39. render_error(
  40. message: '自动分配失败',
  41. errors: [e.message],
  42. code: 'AUTO_ASSIGN_ERROR'
  43. )
  44. end
  45. # POST /api/v1/reading_events/:reading_event_id/leader_assignments/:schedule_id/claim
  46. # 自由报名领读
  47. def claim_leadership
  48. schedule = @reading_event.reading_schedules.find(params[:schedule_id])
  49. service = LeaderAssignmentService.claim_leadership!(@reading_event, current_user, schedule)
  50. if service.success?
  51. render_success(
  52. data: service.result[:schedule_data],
  53. message: service.result[:message]
  54. )
  55. log_api_call('leader_assignments#claim_leadership')
  56. else
  57. render_error(
  58. message: service.error_message,
  59. code: 'CLAIM_LEADERSHIP_FAILED',
  60. status: :unprocessable_entity
  61. )
  62. end
  63. rescue ActiveRecord::RecordNotFound
  64. render_error(
  65. message: '阅读计划不存在',
  66. code: 'SCHEDULE_NOT_FOUND',
  67. status: :not_found
  68. )
  69. rescue => e
  70. render_error(
  71. message: '报名领读失败',
  72. errors: [e.message],
  73. code: 'CLAIM_LEADERSHIP_ERROR'
  74. )
  75. end
  76. # POST /api/v1/reading_events/:reading_event_id/leader_assignments/:schedule_id/reassign
  77. # 重新分配领读人
  78. def reassign_leader
  79. schedule = @reading_event.reading_schedules.find(params[:schedule_id])
  80. unless params[:new_leader_id].present?
  81. render_error(
  82. message: '请指定新的领读人',
  83. code: 'NEW_LEADER_REQUIRED',
  84. status: :unprocessable_entity
  85. )
  86. return
  87. end
  88. new_leader = User.find(params[:new_leader_id])
  89. unless new_leader
  90. render_error(
  91. message: '新领读人不存在',
  92. code: 'NEW_LEADER_NOT_FOUND',
  93. status: :not_found
  94. )
  95. return
  96. end
  97. service = LeaderAssignmentService.reassign_leader!(@reading_event, schedule, new_leader)
  98. if service.success?
  99. render_success(
  100. data: service.result,
  101. message: service.result[:message]
  102. )
  103. log_api_call('leader_assignments#reassign_leader')
  104. else
  105. render_error(
  106. message: service.error_message,
  107. code: 'REASSIGN_LEADER_FAILED',
  108. status: :unprocessable_entity
  109. )
  110. end
  111. rescue ActiveRecord::RecordNotFound
  112. render_error(
  113. message: '阅读计划或用户不存在',
  114. code: 'NOT_FOUND',
  115. status: :not_found
  116. )
  117. rescue => e
  118. render_error(
  119. message: '重新分配失败',
  120. errors: [e.message],
  121. code: 'REASSIGN_LEADER_ERROR'
  122. )
  123. end
  124. # POST /api/v1/reading_events/:reading_event_id/leader_assignments/:schedule_id/backup
  125. # 补位分配
  126. def backup_assignment
  127. schedule = @reading_event.reading_schedules.find(params[:schedule_id])
  128. unless params[:backup_leader_id].present?
  129. render_error(
  130. message: '请指定补位人',
  131. code: 'BACKUP_LEADER_REQUIRED',
  132. status: :unprocessable_entity
  133. )
  134. return
  135. end
  136. backup_leader = User.find(params[:backup_leader_id])
  137. unless backup_leader
  138. render_error(
  139. message: '补位人不存在',
  140. code: 'BACKUP_LEADER_NOT_FOUND',
  141. status: :not_found
  142. )
  143. return
  144. end
  145. service = LeaderAssignmentService.backup_assignment!(@reading_event, schedule, backup_leader)
  146. if service.success?
  147. render_success(
  148. data: service.result,
  149. message: service.result[:message]
  150. )
  151. log_api_call('leader_assignments#backup_assignment')
  152. else
  153. render_error(
  154. message: service.error_message,
  155. code: 'BACKUP_ASSIGNMENT_FAILED',
  156. status: :unprocessable_entity
  157. )
  158. end
  159. rescue ActiveRecord::RecordNotFound
  160. render_error(
  161. message: '阅读计划或用户不存在',
  162. code: 'NOT_FOUND',
  163. status: :not_found
  164. )
  165. rescue => e
  166. render_error(
  167. message: '补位分配失败',
  168. errors: [e.message],
  169. code: 'BACKUP_ASSIGNMENT_ERROR'
  170. )
  171. end
  172. # GET /api/v1/reading_events/:reading_event_id/leader_assignments/statistics
  173. # 获取领读分配统计
  174. def statistics
  175. service = LeaderAssignmentService.assignment_statistics(@reading_event)
  176. if service.success?
  177. render_success(
  178. data: service.result
  179. )
  180. log_api_call('leader_assignments#statistics')
  181. else
  182. render_error(
  183. message: service.error_message,
  184. code: 'STATISTICS_FAILED',
  185. status: :unprocessable_entity
  186. )
  187. end
  188. rescue => e
  189. render_error(
  190. message: '获取统计失败',
  191. errors: [e.message],
  192. code: 'STATISTICS_ERROR'
  193. )
  194. end
  195. # GET /api/v1/reading_events/:reading_event_id/leader_assignments/backup_needed
  196. # 获取需要补位的日程
  197. def backup_needed
  198. backup_schedules = @reading_event.schedules_need_backup
  199. schedule_data = backup_schedules.map do |backup_info|
  200. {
  201. schedule: {
  202. id: backup_info[:schedule].id,
  203. day_number: backup_info[:schedule].day_number,
  204. date: backup_info[:schedule].date,
  205. reading_progress: backup_info[:schedule].reading_progress
  206. },
  207. leader: backup_info[:leader] ? {
  208. id: backup_info[:leader].id,
  209. nickname: backup_info[:leader].nickname,
  210. avatar_url: backup_info[:leader].avatar_url
  211. } : nil,
  212. backup_priority: backup_info[:backup_priority],
  213. missing_content: backup_info[:missing_content],
  214. missing_flowers: backup_info[:missing_flowers],
  215. needs_backup: backup_info[:needs_backup],
  216. content_deadline: backup_info[:content_deadline],
  217. flowers_deadline: backup_info[:flowers_deadline]
  218. }
  219. end
  220. render_success(
  221. data: {
  222. backup_schedules: schedule_data,
  223. total_needing_backup: schedule_data.count,
  224. content_deadline_soon: schedule_data.select { |s| s[:missing_content] && s[:content_deadline] <= Date.today + 1.day }.count,
  225. flowers_deadline_soon: schedule_data.select { |s| s[:missing_flowers] && s[:flowers_deadline] <= Date.today + 1.day }.count
  226. }
  227. )
  228. log_api_call('leader_assignments#backup_needed')
  229. rescue => e
  230. render_error(
  231. message: '获取补位信息失败',
  232. errors: [e.message],
  233. code: 'BACKUP_NEEDED_ERROR'
  234. )
  235. end
  236. # GET /api/v1/reading_events/:reading_event_id/leader_assignments/permissions
  237. # 检查领读权限
  238. def check_permissions
  239. schedule = params[:schedule_id] ? @reading_event.reading_schedules.find(params[:schedule_id]) : nil
  240. service = LeaderAssignmentService.check_permissions(@reading_event, current_user, schedule)
  241. if service.success?
  242. render_success(
  243. data: service.result
  244. )
  245. log_api_call('leader_assignments#check_permissions')
  246. else
  247. render_error(
  248. message: service.error_message,
  249. code: 'PERMISSION_CHECK_FAILED',
  250. status: :unprocessable_entity
  251. )
  252. end
  253. rescue ActiveRecord::RecordNotFound
  254. render_error(
  255. message: '阅读计划不存在',
  256. code: 'SCHEDULE_NOT_FOUND',
  257. status: :not_found
  258. )
  259. rescue => e
  260. render_error(
  261. message: '权限检查失败',
  262. errors: [e.message],
  263. code: 'PERMISSION_CHECK_ERROR'
  264. )
  265. end
  266. private
  267. def set_reading_event
  268. event_id = params[:reading_event_id]
  269. @reading_event = ReadingEvent.find(event_id)
  270. rescue ActiveRecord::RecordNotFound
  271. render_error(
  272. message: '活动不存在',
  273. code: 'EVENT_NOT_FOUND',
  274. status: :not_found
  275. )
  276. end
  277. def check_event_permissions
  278. unless @reading_event.leader == current_user
  279. render_error(
  280. message: '只有活动创建者可以管理领读分配',
  281. code: 'FORBIDDEN',
  282. status: :forbidden
  283. )
  284. end
  285. end
  286. def get_assignment_statistics
  287. service = LeaderAssignmentService.assignment_statistics(@reading_event)
  288. service.success? ? service.result : {}
  289. end
  290. end

app/controllers/api/v1/notifications_controller.rb

0.0% lines covered

145 relevant lines. 0 lines covered and 145 lines missed.
    
  1. # frozen_string_literal: true
  2. class Api::V1::NotificationsController < ApplicationController
  3. before_action :authenticate_user!
  4. before_action :set_notification, only: [:show, :update, :destroy]
  5. # GET /api/v1/notifications
  6. # 获取用户的通知列表
  7. def index
  8. page = params[:page] || 1
  9. limit = params[:limit] || 20
  10. notification_type = params[:type]
  11. read_status = params[:read_status] # 'read', 'unread', or nil for all
  12. notifications = current_user.received_notifications.includes(:actor, :notifiable)
  13. # 按类型过滤
  14. notifications = notifications.by_type(notification_type) if notification_type.present?
  15. # 按读取状态过滤
  16. case read_status
  17. when 'read'
  18. notifications = notifications.read
  19. when 'unread'
  20. notifications = notifications.unread
  21. end
  22. # 分页
  23. total_count = notifications.count
  24. notifications = notifications.offset((page - 1) * limit).limit(limit)
  25. render json: {
  26. success: true,
  27. notifications: notifications.map { |n| n.as_json_for_api(include_actor: true, include_notifiable: true) },
  28. pagination: {
  29. current_page: page.to_i,
  30. total_count: total_count,
  31. total_pages: (total_count.to_f / limit).ceil,
  32. has_next: (page.to_i * limit) < total_count,
  33. has_prev: page.to_i > 1
  34. },
  35. stats: {
  36. unread_count: current_user.received_notifications.unread.count,
  37. total_count: total_count
  38. }
  39. }
  40. end
  41. # GET /api/v1/notifications/:id
  42. # 获取单个通知详情
  43. def show
  44. render json: {
  45. success: true,
  46. notification: @notification.as_json_for_api(include_actor: true, include_notifiable: true)
  47. }
  48. end
  49. # PATCH /api/v1/notifications/:id
  50. # 标记通知为已读
  51. def update
  52. if @notification.mark_as_read!
  53. render json: {
  54. success: true,
  55. message: '通知已标记为已读',
  56. notification: @notification.as_json_for_api
  57. }
  58. else
  59. render json: {
  60. success: false,
  61. error: '标记通知失败'
  62. }, status: :unprocessable_entity
  63. end
  64. end
  65. # DELETE /api/v1/notifications/:id
  66. # 删除通知
  67. def destroy
  68. if @notification.destroy
  69. render json: {
  70. success: true,
  71. message: '通知已删除'
  72. }
  73. else
  74. render json: {
  75. success: false,
  76. error: '删除通知失败'
  77. }, status: :unprocessable_entity
  78. end
  79. end
  80. # POST /api/v1/notifications/mark_all_read
  81. # 批量标记所有通知为已读
  82. def mark_all_read
  83. count = NotificationService.mark_all_as_read_for(current_user)
  84. render json: {
  85. success: true,
  86. message: "已标记 #{count} 条通知为已读",
  87. marked_count: count
  88. }
  89. end
  90. # DELETE /api/v1/notifications/batch
  91. # 批量删除通知
  92. def batch_destroy
  93. notification_ids = params[:notification_ids] || []
  94. if notification_ids.blank?
  95. return render json: {
  96. success: false,
  97. error: '请选择要删除的通知'
  98. }, status: :bad_request
  99. end
  100. deleted_count = NotificationService.delete_notifications(notification_ids, current_user)
  101. render json: {
  102. success: true,
  103. message: "已删除 #{deleted_count} 条通知",
  104. deleted_count: deleted_count
  105. }
  106. end
  107. # GET /api/v1/notifications/unread_count
  108. # 获取未读通知数量
  109. def unread_count
  110. count = NotificationService.unread_count_for(current_user)
  111. render json: {
  112. success: true,
  113. unread_count: count
  114. }
  115. end
  116. # GET /api/v1/notifications/stats
  117. # 获取通知统计信息
  118. def stats
  119. days = params[:days]&.to_i || 7
  120. stats = NotificationService.notification_stats_for(current_user, days)
  121. render json: {
  122. success: true,
  123. stats: stats,
  124. period: "#{days} 天"
  125. }
  126. end
  127. # GET /api/v1/notifications/recent
  128. # 获取最近的通知
  129. def recent
  130. limit = params[:limit]&.to_i || 5
  131. include_read = params[:include_read] == 'true'
  132. notifications = NotificationService.recent_notifications_for(current_user, limit, include_read)
  133. render json: {
  134. success: true,
  135. notifications: notifications.map { |n| n.as_json_for_api(include_actor: true) }
  136. }
  137. end
  138. # GET /api/v1/notifications/check_new
  139. # 检查是否有新通知
  140. def check_new
  141. since = params[:since]&.to_time
  142. has_new = NotificationService.has_new_notifications?(current_user, since: since)
  143. render json: {
  144. success: true,
  145. has_new: has_new,
  146. unread_count: NotificationService.unread_count_for(current_user)
  147. }
  148. end
  149. # POST /api/v1/notifications/test
  150. # 测试通知(仅开发环境)
  151. def test
  152. return render json: { error: '此功能仅在开发环境中可用' }, status: :forbidden unless Rails.env.development?
  153. # 创建测试通知
  154. test_notification = NotificationService.send_system_notification(
  155. current_user,
  156. '测试通知',
  157. '这是一个测试通知,用于验证通知系统功能。',
  158. actor: current_user
  159. )
  160. render json: {
  161. success: true,
  162. message: '测试通知已创建',
  163. notification: test_notification.first&.as_json_for_api
  164. }
  165. end
  166. private
  167. # 设置通知
  168. def set_notification
  169. @notification = current_user.received_notifications.find(params[:id])
  170. rescue ActiveRecord::RecordNotFound
  171. render json: { error: '通知不存在' }, status: :not_found
  172. end
  173. end

app/controllers/api/v1/performance_posts_controller.rb

0.0% lines covered

286 relevant lines. 0 lines covered and 286 lines missed.
    
  1. # frozen_string_literal: true
  2. module Api
  3. module V1
  4. # PerformancePostsController - 高性能Posts控制器
  5. # 集成所有性能优化策略:索引优化、N+1查询解决、分页优化、缓存策略
  6. class PerformancePostsController < Api::V1::BaseController
  7. before_action :authenticate_user!
  8. # GET /api/v1/performance_posts
  9. # 高性能帖子列表,支持cursor分页和缓存
  10. def index
  11. # 解析参数
  12. filters = parse_filters
  13. pagination_options = parse_pagination_options
  14. cache_options = parse_cache_options
  15. # 使用缓存获取帖子列表
  16. if should_use_cache?
  17. posts_data = QueryCacheService.fetch_posts_list(
  18. filters,
  19. page: pagination_options[:page],
  20. per_page: pagination_options[:per_page],
  21. current_user: current_user
  22. )
  23. # 构建分页信息
  24. if pagination_options[:cursor]
  25. # Cursor分页信息
  26. total_count = nil
  27. pagination_info = cursor_pagination_info(posts_data, pagination_options)
  28. else
  29. # 传统分页信息
  30. total_count = Post.visible.count
  31. pagination_info = offset_pagination_info(total_count, pagination_options)
  32. end
  33. render json: {
  34. posts: posts_data.map { |post| serialize_post(post, lite: true) },
  35. pagination: pagination_info,
  36. cached: true,
  37. performance: {
  38. query_time_ms: 5, # 缓存命中时的时间
  39. cache_hit: true
  40. }
  41. }
  42. else
  43. # 直接查询(不使用缓存)
  44. posts_data = execute_direct_query(filters, pagination_options)
  45. render json: {
  46. posts: posts_data[:posts].map { |post| serialize_post(post, lite: true) },
  47. pagination: posts_data[:pagination],
  48. cached: false,
  49. performance: {
  50. query_time_ms: 150, # 直接查询的预估时间
  51. cache_hit: false
  52. }
  53. }
  54. end
  55. end
  56. # GET /api/v1/performance_posts/:id
  57. # 高性能帖子详情,支持缓存
  58. def show
  59. # 使用缓存获取帖子详情
  60. post = QueryCacheService.fetch_post(params[:id], current_user: current_user)
  61. # 检查权限
  62. unless current_user.any_admin?
  63. if post.hidden?
  64. return render json: { error: "帖子已被隐藏" }, status: :not_found
  65. end
  66. end
  67. render json: {
  68. post: serialize_post(post),
  69. cached: true,
  70. performance: {
  71. query_time_ms: 3,
  72. cache_hit: true
  73. }
  74. }
  75. end
  76. # POST /api/v1/performance_posts
  77. # 创建帖子,同时清除相关缓存
  78. def create
  79. service_result = PostServiceFacade.create_with_data(current_user, post_params)
  80. if service_result.success?
  81. # 清除相关缓存
  82. clear_related_caches
  83. render json: {
  84. post: service_result.data[:post],
  85. message: "帖子创建成功",
  86. performance: {
  87. cache_cleared: true
  88. }
  89. }, status: :created
  90. else
  91. render json: { errors: service_result.error_messages }, status: :unprocessable_entity
  92. end
  93. end
  94. # GET /api/v1/performance_posts/stats
  95. # 帖子统计信息,使用缓存
  96. def stats
  97. stats_data = QueryCacheService.fetch("posts_stats:#{Date.current}",
  98. expires_in: 1.hour) do
  99. {
  100. total_posts: Post.visible.count,
  101. total_comments: Comment.joins(:post).where(posts: { hidden: false }).count,
  102. total_likes: Like.joins("INNER JOIN posts ON likes.target_id = posts.id AND likes.target_type = 'Post'")
  103. .where(posts: { hidden: false }).count,
  104. posts_by_category: posts_by_category_stats,
  105. recent_activity: recent_activity_stats
  106. }
  107. end
  108. render json: {
  109. stats: stats_data,
  110. cached: true,
  111. performance: {
  112. query_time_ms: 10
  113. }
  114. }
  115. end
  116. private
  117. # 解析筛选参数
  118. def parse_filters
  119. {
  120. category: params[:category],
  121. user_id: params[:user_id],
  122. date_from: params[:date_from],
  123. date_to: params[:date_to]
  124. }.compact
  125. end
  126. # 解析分页参数
  127. def parse_pagination_options
  128. if params[:cursor].present?
  129. {
  130. cursor: params[:cursor],
  131. per_page: [params[:per_page].to_i, 50].min,
  132. order_field: params[:order]&.to_sym || :created_at,
  133. order_direction: params[:direction]&.to_sym || :desc
  134. }
  135. else
  136. {
  137. page: [params[:page].to_i, 1].max,
  138. per_page: [params[:per_page].to_i, 50].min,
  139. order_field: params[:order]&.to_sym || :created_at,
  140. order_direction: params[:direction]&.to_sym || :desc
  141. }
  142. end
  143. end
  144. # 解析缓存参数
  145. def parse_cache_options
  146. {
  147. use_cache: params[:cache] != 'false',
  148. cache_level: params[:cache_level]&.to_sym || :redis,
  149. expires_in: params[:expires_in]&.to_i || 5.minutes
  150. }
  151. end
  152. # 判断是否使用缓存
  153. def should_use_cache?
  154. cache_options = parse_cache_options
  155. cache_options[:use_cache] && !cache_bypass_required?
  156. end
  157. # 判断是否需要绕过缓存
  158. def cache_bypass_required?
  159. # 用户指定不使用缓存
  160. return true if params[:cache] == 'false'
  161. # 管理员请求实时数据
  162. return true if current_user&.any_admin? && params[:realtime] == 'true'
  163. # 特殊筛选条件不使用缓存
  164. return true if params[:user_id].present? || params[:date_from].present?
  165. false
  166. end
  167. # 执行直接查询
  168. def execute_direct_query(filters, pagination_options)
  169. # 构建基础查询
  170. posts_query = Post.visible.includes(:user)
  171. # 应用筛选
  172. posts_query = apply_filters(posts_query, filters)
  173. # 应用排序
  174. posts_query = apply_ordering(posts_query, pagination_options)
  175. # 应用分页
  176. if pagination_options[:cursor]
  177. result = OptimizedPaginationService.cursor_paginate(
  178. posts_query,
  179. cursor: pagination_options[:cursor],
  180. per_page: pagination_options[:per_page],
  181. order_field: pagination_options[:order_field],
  182. order_direction: pagination_options[:order_direction]
  183. )
  184. else
  185. result = OptimizedPaginationService.paginate(
  186. posts_query,
  187. page: pagination_options[:page],
  188. per_page: pagination_options[:per_page],
  189. order_field: pagination_options[:order_field],
  190. order_direction: pagination_options[:order_direction]
  191. )
  192. end
  193. # 预加载权限和点赞状态
  194. preload_interactions(result.records, current_user) if current_user
  195. {
  196. posts: result.records,
  197. pagination: build_pagination_info(result, pagination_options)
  198. }
  199. end
  200. # 应用筛选条件
  201. def apply_filters(query, filters)
  202. query = query.where(category: filters[:category]) if filters[:category]
  203. query = query.where(user_id: filters[:user_id]) if filters[:user_id]
  204. query = query.where('created_at >= ?', filters[:date_from]) if filters[:date_from]
  205. query = query.where('created_at <= ?', filters[:date_to]) if filters[:date_to]
  206. query
  207. end
  208. # 应用排序
  209. def apply_ordering(query, options)
  210. case options[:order_field]
  211. when :likes_count
  212. query = query.order('likes_count DESC, created_at DESC')
  213. when :comments_count
  214. query = query.order('comments_count DESC, created_at DESC')
  215. else
  216. query = query.order("#{options[:order_field]} #{options[:order_direction].upcase}")
  217. end
  218. query
  219. end
  220. # 预加载交互信息
  221. def preload_interactions(posts, user)
  222. return if posts.empty?
  223. post_ids = posts.map(&:id)
  224. # 批量加载权限
  225. permissions = PostPermissionService.batch_check_posts_permissions(
  226. post_ids, user.id
  227. )
  228. # 批量加载点赞状态
  229. liked_post_ids = Like.where(
  230. user_id: user.id,
  231. target_type: 'Post',
  232. target_id: post_ids
  233. ).pluck(:target_id)
  234. posts.each do |post|
  235. post.instance_variable_set(:@permissions, permissions)
  236. post.instance_variable_set(:@current_user_liked, liked_post_ids.include?(post.id))
  237. end
  238. end
  239. # 序列化帖子
  240. def serialize_post(post, lite: false)
  241. permissions = post.instance_variable_get(:@permissions) || {}
  242. liked_status = post.instance_variable_get(:@current_user_liked)
  243. result = {
  244. id: post.id,
  245. title: post.title,
  246. content: post.content,
  247. category: post.category,
  248. category_name: post.category_name,
  249. pinned: post.pinned,
  250. hidden: post.hidden,
  251. created_at: post.created_at,
  252. updated_at: post.updated_at,
  253. time_ago: post.time_ago_in_words(post.created_at),
  254. stats: {
  255. likes_count: post.likes_count,
  256. comments_count: post.comments_count
  257. },
  258. author: post.user.as_json_for_api
  259. }
  260. # 添加交互信息
  261. if current_user && !lite
  262. result[:interactions] = {
  263. liked: liked_status || false,
  264. can_edit: permissions.dig(:edit, post.id) || false,
  265. can_delete: permissions.dig(:delete, post.id) || false,
  266. can_pin: permissions.dig(:pin, post.id) || false,
  267. can_hide: permissions.dig(:hide, post.id) || false,
  268. can_comment: permissions.dig(:comment, post.id) || false
  269. }
  270. end
  271. result
  272. end
  273. # 构建分页信息
  274. def build_pagination_info(pagination_result, options)
  275. if options[:cursor]
  276. {
  277. type: 'cursor',
  278. next_cursor: pagination_result.next_cursor,
  279. prev_cursor: pagination_result.prev_cursor,
  280. has_next: pagination_result.has_next_page?,
  281. has_prev: pagination_result.has_prev_page?,
  282. per_page: options[:per_page]
  283. }
  284. else
  285. {
  286. type: 'offset',
  287. current_page: pagination_result.current_page,
  288. per_page: options[:per_page],
  289. total_count: pagination_result.total_count,
  290. total_pages: pagination_result.total_pages,
  291. has_next: pagination_result.has_next_page?,
  292. has_prev: pagination_result.has_prev_page?
  293. }
  294. end
  295. end
  296. # 清除相关缓存
  297. def clear_related_caches
  298. patterns = [
  299. 'posts_list:*',
  300. 'post:*',
  301. 'posts_stats:*'
  302. ]
  303. patterns.each do |pattern|
  304. QueryCacheService.clear_cache(pattern)
  305. end
  306. Rails.logger.info "已清除帖子相关缓存"
  307. end
  308. # 统计方法
  309. def posts_by_category_stats
  310. Post.visible.group(:category).count
  311. end
  312. def recent_activity_stats
  313. {
  314. posts_today: Post.visible.where('created_at >= ?', Date.current).count,
  315. comments_today: Comment.joins(:post)
  316. .where(posts: { hidden: false })
  317. .where('comments.created_at >= ?', Date.current)
  318. .count,
  319. likes_today: Like.joins("INNER JOIN posts ON likes.target_id = posts.id AND likes.target_type = 'Post'")
  320. .where(posts: { hidden: false })
  321. .where('likes.created_at >= ?', Date.current)
  322. .count
  323. }
  324. end
  325. def post_params
  326. params.require(:post).permit(:title, :content, :category, :images, tags: [])
  327. end
  328. end
  329. end
  330. end

app/controllers/api/v1/reading_events_controller.rb

0.0% lines covered

354 relevant lines. 0 lines covered and 354 lines missed.
    
  1. class Api::V1::ReadingEventsController < Api::V1::BaseController
  2. before_action :authenticate_user!, except: [:index, :show]
  3. before_action :set_reading_event, only: [:show, :update, :destroy, :start, :complete, :approve, :reject, :statistics]
  4. before_action :authorize_event_leader!, only: [:update, :destroy, :start]
  5. before_action :authorize_admin!, only: [:approve, :reject]
  6. # GET /api/v1/reading_events
  7. # 活动列表和搜索
  8. def index
  9. @reading_events = ReadingEvent.includes(:leader, :event_enrollments)
  10. .filter_by_status(params[:status])
  11. .filter_by_mode(params[:activity_mode])
  12. .filter_by_fee_type(params[:fee_type])
  13. # 关键词搜索
  14. if params[:keyword].present?
  15. keyword = "%#{params[:keyword]}%"
  16. @reading_events = @reading_events.where(
  17. "reading_events.title ILIKE ? OR reading_events.book_name ILIKE ?",
  18. keyword, keyword
  19. )
  20. end
  21. # 时间范围过滤
  22. if params[:start_date_from].present?
  23. start_date = safe_date_param(:start_date_from)
  24. @reading_events = @reading_events.where('reading_events.start_date >= ?', start_date) if start_date
  25. end
  26. if params[:start_date_to].present?
  27. end_date = safe_date_param(:start_date_to)
  28. @reading_events = @reading_events.where('reading_events.start_date <= ?', end_date) if end_date
  29. end
  30. # 排序
  31. sorting = sorting_params(default_field: :created_at)
  32. @reading_events = @reading_events.order("#{sorting[:sort_field]} #{sorting[:sort_direction]}")
  33. # 分页
  34. pagination = pagination_params
  35. @reading_events = @reading_events.page(pagination[:page]).per(pagination[:per_page])
  36. # 构建响应数据
  37. events_data = @reading_events.map do |event|
  38. {
  39. id: event.id,
  40. title: event.title,
  41. book_name: event.book_name,
  42. book_cover_url: event.book_cover_url,
  43. description: event.description,
  44. activity_mode: event.activity_mode,
  45. fee_type: event.fee_type,
  46. fee_amount: event.fee_amount,
  47. start_date: event.start_date,
  48. end_date: event.end_date,
  49. status: event.status,
  50. approval_status: event.approval_status,
  51. participants_count: event.participants_count,
  52. max_participants: event.max_participants,
  53. available_spots: event.available_spots,
  54. leader: {
  55. id: event.leader.id,
  56. nickname: event.leader.nickname
  57. },
  58. created_at: event.created_at
  59. }
  60. end
  61. render_success(
  62. data: events_data,
  63. meta: pagination_meta(@reading_events)
  64. )
  65. log_api_call('reading_events#index')
  66. end
  67. # GET /api/v1/reading_events/:id
  68. # 活动详情
  69. def show
  70. event_data = {
  71. id: @reading_event.id,
  72. title: @reading_event.title,
  73. book_name: @reading_event.book_name,
  74. book_cover_url: @reading_event.book_cover_url,
  75. description: @reading_event.description,
  76. activity_mode: @reading_event.activity_mode,
  77. weekend_rest: @reading_event.weekend_rest,
  78. completion_standard: @reading_event.completion_standard,
  79. leader_assignment_type: @reading_event.leader_assignment_type,
  80. fee_type: @reading_event.fee_type,
  81. fee_amount: @reading_event.fee_amount,
  82. leader_reward_percentage: @reading_event.leader_reward_percentage,
  83. max_participants: @reading_event.max_participants,
  84. min_participants: @reading_event.min_participants,
  85. start_date: @reading_event.start_date,
  86. end_date: @reading_event.end_date,
  87. enrollment_deadline: @reading_event.enrollment_deadline,
  88. status: @reading_event.status,
  89. approval_status: @reading_event.approval_status,
  90. participants_count: @reading_event.participants_count,
  91. available_spots: @reading_event.available_spots,
  92. days_count: @reading_event.days_count,
  93. leader: {
  94. id: @reading_event.leader.id,
  95. nickname: @reading_event.leader.nickname
  96. },
  97. created_at: @reading_event.created_at,
  98. updated_at: @reading_event.updated_at
  99. }
  100. # 如果已登录,添加用户相关信息
  101. if current_user
  102. enrollment = @reading_event.event_enrollments.find_by(user: current_user)
  103. event_data[:user_enrollment] = enrollment ? {
  104. id: enrollment.id,
  105. enrollment_type: enrollment.enrollment_type,
  106. status: enrollment.status,
  107. enrollment_date: enrollment.enrollment_date,
  108. completion_rate: enrollment.completion_rate,
  109. check_ins_count: enrollment.check_ins_count,
  110. flowers_received_count: enrollment.flowers_received_count
  111. } : nil
  112. event_data[:user_permissions] = {
  113. can_enroll: @reading_event.can_enroll? && !enrollment,
  114. can_edit: current_user == @reading_event.leader,
  115. can_start: @reading_event.can_start? && current_user == @reading_event.leader,
  116. is_participant: enrollment&.can_participate? || false
  117. }
  118. end
  119. render_success(data: event_data)
  120. log_api_call('reading_events#show')
  121. end
  122. # POST /api/v1/reading_events
  123. # 创建活动
  124. def create
  125. return unless authenticate_user!
  126. return unless validate_required_fields(:title, :book_name, :start_date, :end_date)
  127. ActiveRecord::Base.transaction do
  128. @reading_event = ReadingEvent.new(reading_event_params)
  129. @reading_event.leader = current_user
  130. @reading_event.status = :draft
  131. if @reading_event.save
  132. event_data = build_event_data(@reading_event)
  133. render_success(
  134. data: event_data,
  135. message: '活动创建成功'
  136. )
  137. log_api_call('reading_events#create')
  138. else
  139. render_error(
  140. message: '活动创建失败',
  141. errors: @reading_event.errors.full_messages,
  142. code: 'VALIDATION_ERROR'
  143. )
  144. end
  145. end
  146. end
  147. # PUT/PATCH /api/v1/reading_events/:id
  148. # 更新活动
  149. def update
  150. ActiveRecord::Base.transaction do
  151. if @reading_event.update(reading_event_params)
  152. event_data = build_event_data(@reading_event)
  153. render_success(
  154. data: event_data,
  155. message: '活动更新成功'
  156. )
  157. log_api_call('reading_events#update')
  158. else
  159. render_error(
  160. message: '活动更新失败',
  161. errors: @reading_event.errors.full_messages,
  162. code: 'VALIDATION_ERROR'
  163. )
  164. end
  165. end
  166. end
  167. # DELETE /api/v1/reading_events/:id
  168. # 删除活动
  169. def destroy
  170. # 只有草稿状态或被拒绝的活动才能删除
  171. unless @reading_event.draft? || @reading_event.rejected?
  172. render_error(
  173. message: '只有草稿状态或被拒绝的活动才能删除',
  174. code: 'CANNOT_DELETE_EVENT'
  175. )
  176. return
  177. end
  178. ActiveRecord::Base.transaction do
  179. @reading_event.destroy!
  180. render_success(message: '活动删除成功')
  181. log_api_call('reading_events#destroy')
  182. end
  183. rescue ActiveRecord::RecordNotDestroyed
  184. render_error(
  185. message: '活动删除失败',
  186. code: 'DELETE_FAILED'
  187. )
  188. end
  189. # POST /api/v1/reading_events/:id/start
  190. # 开始活动
  191. def start
  192. unless @reading_event.can_start?
  193. render_error(
  194. message: '活动当前状态无法开始',
  195. code: 'CANNOT_START_EVENT'
  196. )
  197. return
  198. end
  199. if @reading_event.start!
  200. render_success(
  201. data: build_event_data(@reading_event),
  202. message: '活动已开始'
  203. )
  204. log_api_call('reading_events#start')
  205. else
  206. render_error(
  207. message: '活动开始失败',
  208. code: 'START_FAILED'
  209. )
  210. end
  211. end
  212. # POST /api/v1/reading_events/:id/complete
  213. # 完成活动(管理员或活动创建者)
  214. def complete
  215. unless @reading_event.can_complete?
  216. render_error(
  217. message: '活动当前状态无法完成',
  218. code: 'CANNOT_COMPLETE_EVENT'
  219. )
  220. return
  221. end
  222. if @reading_event.complete!
  223. render_success(
  224. data: build_event_data(@reading_event),
  225. message: '活动已完成'
  226. )
  227. log_api_call('reading_events#complete')
  228. else
  229. render_error(
  230. message: '活动完成失败',
  231. code: 'COMPLETE_FAILED'
  232. )
  233. end
  234. end
  235. # POST /api/v1/reading_events/:id/approve
  236. # 审批通过活动(管理员)
  237. def approve
  238. unless @reading_event.pending_approval?
  239. render_error(
  240. message: '活动当前状态无法审批',
  241. code: 'CANNOT_APPROVE_EVENT'
  242. )
  243. return
  244. end
  245. if @reading_event.approve!(current_user)
  246. render_success(
  247. data: build_event_data(@reading_event),
  248. message: '活动已审批通过'
  249. )
  250. log_api_call('reading_events#approve')
  251. else
  252. render_error(
  253. message: '活动审批失败',
  254. code: 'APPROVE_FAILED'
  255. )
  256. end
  257. end
  258. # POST /api/v1/reading_events/:id/reject
  259. # 拒绝活动(管理员)
  260. def reject
  261. unless @reading_event.pending_approval?
  262. render_error(
  263. message: '活动当前状态无法拒绝',
  264. code: 'CANNOT_REJECT_EVENT'
  265. )
  266. return
  267. end
  268. reason = params[:reason] || '不符合活动规范'
  269. if @reading_event.reject!(current_user, reason)
  270. render_success(
  271. data: build_event_data(@reading_event),
  272. message: '活动已拒绝'
  273. )
  274. log_api_call('reading_events#reject')
  275. else
  276. render_error(
  277. message: '活动拒绝失败',
  278. code: 'REJECT_FAILED'
  279. )
  280. end
  281. end
  282. # GET /api/v1/reading_events/:id/statistics
  283. # 活动统计信息
  284. def statistics
  285. unless @reading_event.in_progress? || @reading_event.completed?
  286. render_error(
  287. message: '活动未开始或已结束,暂无统计数据',
  288. code: 'NO_STATISTICS_AVAILABLE'
  289. )
  290. return
  291. end
  292. stats = @reading_event.completion_statistics
  293. # 添加参与者排行榜
  294. top_participants = @reading_event.event_enrollments
  295. .includes(:user)
  296. .by_completion_rate(:desc)
  297. .limit(10)
  298. .map do |enrollment|
  299. {
  300. user_id: enrollment.user.id,
  301. nickname: enrollment.user.nickname,
  302. completion_rate: enrollment.completion_rate,
  303. check_ins_count: enrollment.check_ins_count,
  304. flowers_received_count: enrollment.flowers_received_count
  305. }
  306. end
  307. statistics_data = {
  308. total_participants: stats[:total_participants],
  309. completed_participants: stats[:completed_participants],
  310. average_completion_rate: stats[:average_completion_rate],
  311. total_check_ins: stats[:total_check_ins],
  312. total_flowers: stats[:total_flowers],
  313. completion_rate: stats[:total_participants] > 0 ?
  314. (stats[:completed_participants].to_f / stats[:total_participants] * 100).round(2) : 0,
  315. top_participants: top_participants
  316. }
  317. render_success(data: statistics_data)
  318. log_api_call('reading_events#statistics')
  319. end
  320. private
  321. def set_reading_event
  322. @reading_event = ReadingEvent.find(params[:id])
  323. rescue ActiveRecord::RecordNotFound
  324. render_error(
  325. message: '活动不存在',
  326. code: 'EVENT_NOT_FOUND',
  327. status: :not_found
  328. )
  329. end
  330. def reading_event_params
  331. {
  332. title: params[:title],
  333. book_name: params[:book_name],
  334. book_cover_url: params[:book_cover_url],
  335. description: params[:description],
  336. activity_mode: params[:activity_mode] || 'note_checkin',
  337. weekend_rest: params[:weekend_rest] == true,
  338. completion_standard: params[:completion_standard]&.to_i || 80,
  339. leader_assignment_type: params[:leader_assignment_type] || 'voluntary',
  340. fee_type: params[:fee_type] || 'free',
  341. fee_amount: params[:fee_amount]&.to_d || 0.0,
  342. leader_reward_percentage: params[:leader_reward_percentage]&.to_d || 20.0,
  343. max_participants: params[:max_participants]&.to_i || 25,
  344. min_participants: params[:min_participants]&.to_i || 10,
  345. start_date: params[:start_date]&.to_date,
  346. end_date: params[:end_date]&.to_date,
  347. enrollment_deadline: params[:enrollment_deadline]&.to_datetime
  348. }.compact
  349. end
  350. def build_event_data(event)
  351. {
  352. id: event.id,
  353. title: event.title,
  354. book_name: event.book_name,
  355. book_cover_url: event.book_cover_url,
  356. description: event.description,
  357. activity_mode: event.activity_mode,
  358. weekend_rest: event.weekend_rest,
  359. completion_standard: event.completion_standard,
  360. leader_assignment_type: event.leader_assignment_type,
  361. fee_type: event.fee_type,
  362. fee_amount: event.fee_amount,
  363. leader_reward_percentage: event.leader_reward_percentage,
  364. max_participants: event.max_participants,
  365. min_participants: event.min_participants,
  366. start_date: event.start_date,
  367. end_date: event.end_date,
  368. enrollment_deadline: event.enrollment_deadline,
  369. status: event.status,
  370. approval_status: event.approval_status,
  371. participants_count: event.participants_count,
  372. available_spots: event.available_spots,
  373. days_count: event.days_count,
  374. leader: {
  375. id: event.leader.id,
  376. nickname: event.leader.nickname
  377. },
  378. created_at: event.created_at,
  379. updated_at: event.updated_at
  380. }
  381. end
  382. end

app/controllers/api/v1/reading_schedules_controller.rb

0.0% lines covered

236 relevant lines. 0 lines covered and 236 lines missed.
    
  1. class Api::V1::ReadingSchedulesController < Api::V1::BaseController
  2. before_action :authenticate_user!
  3. before_action :set_reading_event
  4. before_action :set_reading_schedule, only: [:show, :assign_leader, :remove_leader]
  5. # GET /api/v1/reading_schedules
  6. # 阅读计划列表
  7. def index
  8. # 检查权限:只有活动参与者、创建者可以查看
  9. unless can_view_schedules?
  10. render_error(
  11. message: '权限不足',
  12. code: 'FORBIDDEN',
  13. status: :forbidden
  14. )
  15. return
  16. end
  17. # 获取阅读计划列表
  18. schedules = @reading_event.reading_schedules
  19. .includes(:daily_leader, :daily_leading, :check_ins, :flowers)
  20. .chronological
  21. # 分页
  22. pagination = pagination_params
  23. schedules = schedules.page(pagination[:page]).per(pagination[:per_page])
  24. # 构建响应数据
  25. schedules_data = schedules.map do |schedule|
  26. build_schedule_data(schedule, detailed: true)
  27. end
  28. render_success(
  29. data: schedules_data,
  30. meta: pagination_meta(schedules)
  31. )
  32. log_api_call('reading_schedules#index')
  33. end
  34. # GET /api/v1/reading_schedules/:id
  35. # 阅读计划详情
  36. def show
  37. unless @reading_schedule
  38. render_error(
  39. message: '阅读计划不存在',
  40. code: 'SCHEDULE_NOT_FOUND',
  41. status: :not_found
  42. )
  43. return
  44. end
  45. # 检查权限
  46. unless can_view_schedule?(@reading_schedule)
  47. render_error(
  48. message: '权限不足',
  49. code: 'FORBIDDEN',
  50. status: :forbidden
  51. )
  52. return
  53. end
  54. schedule_data = build_schedule_data(@reading_schedule, detailed: true)
  55. render_success(data: schedule_data)
  56. log_api_call('reading_schedules#show')
  57. end
  58. # POST /api/v1/reading_schedules/:id/assign_leader
  59. # 分配领读人
  60. def assign_leader
  61. unless @reading_schedule
  62. render_error(
  63. message: '阅读计划不存在',
  64. code: 'SCHEDULE_NOT_FOUND',
  65. status: :not_found
  66. )
  67. return
  68. end
  69. # 检查权限:只有活动创建者可以分配领读人
  70. unless @reading_event.leader == current_user
  71. render_error(
  72. message: '只有活动创建者可以分配领读人',
  73. code: 'FORBIDDEN',
  74. status: :forbidden
  75. )
  76. return
  77. end
  78. # 检查用户参数
  79. return unless validate_required_fields(:user_id)
  80. target_user = User.find(params[:user_id])
  81. unless target_user
  82. render_error(
  83. message: '用户不存在',
  84. code: 'USER_NOT_FOUND',
  85. status: :not_found
  86. )
  87. return
  88. end
  89. # 检查用户是否是活动参与者
  90. unless @reading_event.participants.include?(target_user)
  91. render_error(
  92. message: '只能分配活动的参与者作为领读人',
  93. code: 'USER_NOT_PARTICIPANT',
  94. status: :unprocessable_entity
  95. )
  96. return
  97. end
  98. ActiveRecord::Base.transaction do
  99. if @reading_schedule.assign_leader!(target_user)
  100. schedule_data = build_schedule_data(@reading_schedule, detailed: true)
  101. render_success(
  102. data: schedule_data,
  103. message: '领读人分配成功'
  104. )
  105. log_api_call('reading_schedules#assign_leader')
  106. else
  107. render_error(
  108. message: '领读人分配失败',
  109. code: 'ASSIGN_LEADER_FAILED'
  110. )
  111. end
  112. end
  113. rescue => e
  114. render_error(
  115. message: '领读人分配失败',
  116. errors: [e.message],
  117. code: 'ASSIGN_LEADER_ERROR'
  118. )
  119. end
  120. # POST /api/v1/reading_schedules/:id/remove_leader
  121. # 移除领读人
  122. def remove_leader
  123. unless @reading_schedule
  124. render_error(
  125. message: '阅读计划不存在',
  126. code: 'SCHEDULE_NOT_FOUND',
  127. status: :not_found
  128. )
  129. return
  130. end
  131. # 检查权限:只有活动创建者可以移除领读人
  132. unless @reading_event.leader == current_user
  133. render_error(
  134. message: '只有活动创建者可以移除领读人',
  135. code: 'FORBIDDEN',
  136. status: :forbidden
  137. )
  138. return
  139. end
  140. ActiveRecord::Base.transaction do
  141. if @reading_schedule.remove_leader!
  142. schedule_data = build_schedule_data(@reading_schedule, detailed: true)
  143. render_success(
  144. data: schedule_data,
  145. message: '领读人移除成功'
  146. )
  147. log_api_call('reading_schedules#remove_leader')
  148. else
  149. render_error(
  150. message: '领读人移除失败',
  151. code: 'REMOVE_LEADER_FAILED'
  152. )
  153. end
  154. end
  155. rescue => e
  156. render_error(
  157. message: '领读人移除失败',
  158. errors: [e.message],
  159. code: 'REMOVE_LEADER_ERROR'
  160. )
  161. end
  162. private
  163. def set_reading_event
  164. event_id = params[:reading_event_id]
  165. @reading_event = ReadingEvent.find(event_id)
  166. rescue ActiveRecord::RecordNotFound
  167. render_error(
  168. message: '活动不存在',
  169. code: 'EVENT_NOT_FOUND',
  170. status: :not_found
  171. )
  172. end
  173. def set_reading_schedule
  174. @reading_schedule = @reading_event.reading_schedules.find(params[:id])
  175. rescue ActiveRecord::RecordNotFound
  176. @reading_schedule = nil
  177. end
  178. def build_schedule_data(schedule, detailed: false)
  179. data = {
  180. id: schedule.id,
  181. day_number: schedule.day_number,
  182. date: schedule.date,
  183. reading_progress: schedule.reading_progress,
  184. daily_leader: schedule.daily_leader ? {
  185. id: schedule.daily_leader.id,
  186. nickname: schedule.daily_leader.nickname,
  187. avatar_url: schedule.daily_leader.avatar_url
  188. } : nil,
  189. created_at: schedule.created_at,
  190. updated_at: schedule.updated_at
  191. }
  192. if detailed
  193. data[:reading_event] = {
  194. id: schedule.reading_event.id,
  195. title: schedule.reading_event.title,
  196. book_name: schedule.reading_event.book_name,
  197. status: schedule.reading_event.status,
  198. activity_mode: schedule.reading_event.activity_mode
  199. }
  200. data[:daily_leading] = schedule.daily_leading ? {
  201. id: schedule.daily_leading.id,
  202. content: schedule.daily_leading.content,
  203. reading_pages: schedule.daily_leading.reading_pages,
  204. created_at: schedule.daily_leading.created_at,
  205. updated_at: schedule.daily_leading.updated_at
  206. } : nil
  207. data[:statistics] = schedule.participation_statistics
  208. data[:status_info] = {
  209. today?: schedule.today?,
  210. past?: schedule.past?,
  211. future?: schedule.future?,
  212. current_day?: schedule.current_day?,
  213. completed?: schedule.completed?,
  214. has_check_ins?: schedule.has_check_ins?,
  215. has_flowers?: schedule.has_flowers?,
  216. has_leading_content?: schedule.has_leading_content?
  217. }
  218. data[:permissions] = {
  219. can_view: can_view_schedule?(schedule),
  220. can_assign_leader: can_assign_leader?(schedule),
  221. can_remove_leader: can_remove_leader?(schedule),
  222. can_publish_content: schedule.can_publish_leading_content?,
  223. can_give_flowers: schedule.can_give_flowers?,
  224. needs_backup: schedule.needs_backup?,
  225. backup_permissions: schedule.backup_permissions
  226. }
  227. data[:leading_status] = {
  228. content: schedule.leading_content_status,
  229. flowers: schedule.flower_giving_status
  230. }
  231. end
  232. data
  233. end
  234. # 权限检查方法
  235. def can_view_schedules?
  236. return true if @reading_event.leader == current_user
  237. return true if @reading_event.participants.include?(current_user)
  238. false
  239. end
  240. def can_view_schedule?(schedule)
  241. return true if @reading_event.leader == current_user
  242. return true if schedule.daily_leader == current_user
  243. return true if @reading_event.participants.include?(current_user)
  244. false
  245. end
  246. def can_assign_leader?(schedule)
  247. return false unless @reading_event.leader == current_user
  248. schedule.can_assign_leader?
  249. end
  250. def can_remove_leader?(schedule)
  251. return false unless @reading_event.leader == current_user
  252. schedule.daily_leader.present?
  253. end
  254. end

app/controllers/application_controller.rb

0.0% lines covered

36 relevant lines. 0 lines covered and 36 lines missed.
    
  1. class ApplicationController < ActionController::API
  2. include Authenticable
  3. # API健康检查端点
  4. def health
  5. response_data = {
  6. status: "ok",
  7. timestamp: Time.current.iso8601,
  8. version: "1.0.0",
  9. environment: Rails.env
  10. }
  11. render json: response_data
  12. end
  13. private
  14. def check_database_status
  15. begin
  16. ActiveRecord::Base.connection.execute("SELECT 1")
  17. "connected"
  18. rescue => e
  19. "error: #{e.message}"
  20. end
  21. end
  22. def check_permissions_status
  23. begin
  24. # 检查权限相关的关键组件
  25. status = {}
  26. # 检查User模型
  27. status[:user_model] = User.respond_to?(:any_admin?) ? "ok" : "missing_methods"
  28. # 检查AdminAuthorizable
  29. status[:admin_authorizable] = defined?(AdminAuthorizable) ? "ok" : "missing"
  30. # 检查角色枚举
  31. if User.respond_to?(:roles)
  32. status[:role_enums] = User.roles.keys.join(",")
  33. else
  34. status[:role_enums] = "not_defined"
  35. end
  36. status
  37. rescue => e
  38. { error: e.message }
  39. end
  40. end
  41. end

app/controllers/concerns/admin_authorizable.rb

0.0% lines covered

70 relevant lines. 0 lines covered and 70 lines missed.
    
  1. # 管理员权限验证 concern
  2. module AdminAuthorizable
  3. extend ActiveSupport::Concern
  4. # 检查是否是管理员或root用户
  5. def authenticate_admin!
  6. unless current_user&.any_admin?
  7. render json: {
  8. error: "需要管理员权限",
  9. details: {
  10. required_role: "admin 或 root",
  11. current_role: current_user&.role_display_name || "未登录"
  12. }
  13. }, status: :forbidden
  14. end
  15. end
  16. # 检查是否是root用户
  17. def authenticate_root!
  18. unless current_user&.root?
  19. render json: {
  20. error: "需要超级管理员权限",
  21. details: {
  22. required_role: "root",
  23. current_role: current_user&.role_display_name || "未登录"
  24. }
  25. }, status: :unauthorized
  26. end
  27. end
  28. # 检查用户是否有特定权限
  29. def authorize_permission!(permission)
  30. unless current_user&.has_permission?(permission)
  31. render json: {
  32. error: "权限不足",
  33. details: {
  34. required_permission: permission,
  35. current_role: current_user&.role_display_name || "未登录"
  36. }
  37. }, status: :forbidden
  38. end
  39. end
  40. # 检查是否有审批活动权限
  41. def authorize_event_approval!
  42. authorize_permission!(:approve_events)
  43. end
  44. # 检查是否有管理用户权限
  45. def authorize_user_management!
  46. authorize_permission!(:manage_users)
  47. end
  48. # 检查是否有查看管理面板权限
  49. def authorize_admin_panel!
  50. authorize_permission!(:view_admin_panel)
  51. end
  52. # 检查是否有系统管理权限
  53. def authorize_system_management!
  54. authorize_permission!(:manage_system)
  55. end
  56. private
  57. # 辅助方法:检查当前用户是否是管理员
  58. def current_user_admin?
  59. current_user&.any_admin?
  60. end
  61. # 辅助方法:检查当前用户是否是root
  62. def current_user_root?
  63. current_user&.root?
  64. end
  65. # 辅助方法:获取用户角色信息
  66. def user_role_info(user = current_user)
  67. return { role: "未登录", permissions: [] } unless user
  68. {
  69. role: user.role_display_name,
  70. permissions: user_permissions(user)
  71. }
  72. end
  73. # 辅助方法:获取用户权限列表
  74. def user_permissions(user)
  75. permissions = []
  76. permissions << "approve_events" if user.can_approve_events?
  77. permissions << "manage_users" if user.can_manage_users?
  78. permissions << "view_admin_panel" if user.can_view_admin_panel?
  79. permissions << "manage_system" if user.can_manage_system?
  80. permissions
  81. end
  82. end

app/controllers/concerns/api_response.rb

0.0% lines covered

51 relevant lines. 0 lines covered and 51 lines missed.
    
  1. # frozen_string_literal: true
  2. module ApiResponse
  3. extend ActiveSupport::Concern
  4. # 成功响应
  5. def render_success(data = nil, message: nil, status: :ok)
  6. response = {
  7. success: true,
  8. data: data
  9. }
  10. response[:message] = message if message.present?
  11. render json: response, status: status
  12. end
  13. # 创建成功响应
  14. def render_created(data = nil, message: '创建成功')
  15. render_success(data, message: message, status: :created)
  16. end
  17. # 错误响应
  18. def render_error(message, errors: nil, status: :unprocessable_entity)
  19. response = {
  20. success: false,
  21. error: message
  22. }
  23. response[:errors] = errors if errors.present?
  24. render json: response, status: status
  25. end
  26. # 未找到响应
  27. def render_not_found(message = '资源不存在')
  28. render_error(message, status: :not_found)
  29. end
  30. # 权限不足响应
  31. def render_forbidden(message = '无权限访问')
  32. render_error(message, status: :forbidden)
  33. end
  34. # 未认证响应
  35. def render_unauthorized(message = '请先登录')
  36. render_error(message, status: :unauthorized)
  37. end
  38. # 分页响应
  39. def render_paginated(data, pagination_info = {}, message: nil)
  40. response = {
  41. success: true,
  42. data: data,
  43. pagination: pagination_info
  44. }
  45. response[:message] = message if message.present?
  46. render json: response
  47. end
  48. private
  49. # 构建分页信息
  50. def build_pagination_info(collection)
  51. {
  52. current_page: collection.current_page,
  53. total_pages: collection.total_pages,
  54. total_count: collection.total_count,
  55. per_page: collection.limit_value,
  56. has_next_page: collection.next_page.present?,
  57. has_prev_page: collection.prev_page.present?
  58. }
  59. end
  60. end

app/controllers/concerns/api_response_formatter.rb

0.0% lines covered

182 relevant lines. 0 lines covered and 182 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApiResponseFormatter - API响应格式化模块
  3. # 提供统一的API响应格式和成功响应处理
  4. module ApiResponseFormatter
  5. extend ActiveSupport::Concern
  6. # 成功响应格式
  7. def render_success_response(data: nil, message: 'Success', meta: {})
  8. response_data = {
  9. success: true,
  10. message: message,
  11. data: data,
  12. timestamp: Time.current.iso8601,
  13. request_id: request&.request_id || SecureRandom.uuid
  14. }
  15. # 添加元数据
  16. response_data[:meta] = meta if meta.any?
  17. render json: response_data, status: :ok
  18. end
  19. # 分页响应格式
  20. def render_paginated_response(data:, pagination:, message: 'Success', meta: {})
  21. pagination_meta = {
  22. current_page: pagination[:current_page],
  23. per_page: pagination[:per_page],
  24. total_count: pagination[:total_count],
  25. total_pages: pagination[:total_pages]
  26. }
  27. # 添加cursor分页信息
  28. if pagination[:next_cursor]
  29. pagination_meta[:next_cursor] = pagination[:next_cursor]
  30. end
  31. if pagination[:prev_cursor]
  32. pagination_meta[:prev_cursor] = pagination[:prev_cursor]
  33. end
  34. response_data = {
  35. success: true,
  36. message: message,
  37. data: data,
  38. pagination: pagination_meta,
  39. timestamp: Time.current.iso8601,
  40. request_id: request&.request_id || SecureRandom.uuid
  41. }
  42. response_data[:meta] = meta if meta.any?
  43. render json: response_data, status: :ok
  44. end
  45. # 创建成功响应
  46. def render_created_response(data: nil, message: 'Created successfully', location: nil)
  47. response_data = {
  48. success: true,
  49. message: message,
  50. data: data,
  51. timestamp: Time.current.iso8601,
  52. request_id: request&.request_id || SecureRandom.uuid
  53. }
  54. # 设置Location头
  55. headers['Location'] = location if location
  56. render json: response_data, status: :created
  57. end
  58. # 更新成功响应
  59. def render_updated_response(data: nil, message: 'Updated successfully')
  60. response_data = {
  61. success: true,
  62. message: message,
  63. data: data,
  64. timestamp: Time.current.iso8601,
  65. request_id: request&.request_id || SecureRandom.uuid
  66. }
  67. render json: response_data, status: :ok
  68. end
  69. # 删除成功响应
  70. def render_deleted_response(message: 'Deleted successfully')
  71. response_data = {
  72. success: true,
  73. message: message,
  74. timestamp: Time.current.iso8601,
  75. request_id: request&.request_id || SecureRandom.uuid
  76. }
  77. render json: response_data, status: :ok
  78. end
  79. # 无内容响应
  80. def render_no_content_response(message: 'No content')
  81. head :no_content
  82. end
  83. # 批量操作响应
  84. def render_batch_response(results:, message: 'Batch operation completed')
  85. success_count = results.count { |r| r[:success] }
  86. error_count = results.count { |r| !r[:success] }
  87. response_data = {
  88. success: true,
  89. message: message,
  90. data: {
  91. total: results.length,
  92. success_count: success_count,
  93. error_count: error_count,
  94. results: results
  95. },
  96. timestamp: Time.current.iso8601,
  97. request_id: request&.request_id || SecureRandom.uuid
  98. }
  99. render json: response_data, status: :ok
  100. end
  101. # 验证错误响应(用于手动验证)
  102. def render_validation_errors_response(errors:, message: 'Validation failed')
  103. error_response = {
  104. success: false,
  105. error: message,
  106. error_code: 'VALIDATION_ERROR',
  107. error_type: 'validation_error',
  108. errors: errors.is_a?(Hash) ? errors.values.flatten : errors,
  109. timestamp: Time.current.iso8601,
  110. request_id: request&.request_id || SecureRandom.uuid,
  111. details: {
  112. suggestions: [
  113. '请检查必填字段是否完整',
  114. '确认数据格式是否正确',
  115. '参考API文档确认参数要求'
  116. ]
  117. }
  118. }
  119. render json: error_response, status: :unprocessable_entity
  120. end
  121. # 参数错误响应(用于手动参数验证)
  122. def render_parameter_error_response(parameter:, message: nil)
  123. error_message = message || "参数错误: #{parameter}"
  124. error_response = {
  125. success: false,
  126. error: error_message,
  127. error_code: 'INVALID_PARAMETER',
  128. error_type: 'parameter_error',
  129. timestamp: Time.current.iso8601,
  130. request_id: request&.request_id || SecureRandom.uuid,
  131. details: {
  132. parameter: parameter,
  133. suggestions: [
  134. '请检查请求参数格式',
  135. '参考API文档确认参数要求',
  136. '确保参数值符合预期类型和范围'
  137. ]
  138. }
  139. }
  140. render json: error_response, status: :unprocessable_entity
  141. end
  142. # 权限错误响应(用于手动权限检查)
  143. def render_permission_denied_response(message: 'Permission denied')
  144. error_response = {
  145. success: false,
  146. error: message,
  147. error_code: 'PERMISSION_DENIED',
  148. error_type: 'authorization_error',
  149. timestamp: Time.current.iso8601,
  150. request_id: request&.request_id || SecureRandom.uuid,
  151. details: {
  152. user_id: current_user&.id,
  153. user_role: current_user&.role_as_string,
  154. suggestions: [
  155. '请确认您有足够的权限执行此操作',
  156. '如需权限提升,请联系管理员',
  157. '检查用户账户状态是否正常'
  158. ]
  159. }
  160. }
  161. render json: error_response, status: :forbidden
  162. end
  163. # 资源不存在响应(用于手动检查)
  164. def render_not_found_response(resource: 'Resource', message: nil)
  165. error_message = message || "#{resource} not found"
  166. error_response = {
  167. success: false,
  168. error: error_message,
  169. error_code: 'RESOURCE_NOT_FOUND',
  170. error_type: 'not_found',
  171. timestamp: Time.current.iso8601,
  172. request_id: request&.request_id || SecureRandom.uuid,
  173. details: {
  174. resource: resource,
  175. suggestions: [
  176. '请检查资源ID是否正确',
  177. '确认资源是否存在且未被删除',
  178. '检查URL路径是否正确'
  179. ]
  180. }
  181. }
  182. render json: error_response, status: :not_found
  183. end
  184. private
  185. # 构建标准元数据
  186. def build_meta_data(additional_meta = {})
  187. base_meta = {
  188. version: api_version,
  189. environment: Rails.env
  190. }
  191. base_meta.merge(additional_meta)
  192. end
  193. # 获取API版本
  194. def api_version
  195. if request.path.start_with?('/api/v1/')
  196. 'v1'
  197. else
  198. 'v0'
  199. end
  200. end
  201. end

app/controllers/concerns/api_security.rb

0.0% lines covered

279 relevant lines. 0 lines covered and 279 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApiSecurity - API安全增强模块
  3. # 提供API安全相关功能,包括限流、CSRF保护、请求验证等
  4. module ApiSecurity
  5. extend ActiveSupport::Concern
  6. included do
  7. # 添加请求ID追踪
  8. before_action :set_request_id
  9. # 添加安全头
  10. before_action :set_security_headers
  11. # API限流检查
  12. before_action :check_rate_limits
  13. # 参数安全检查
  14. before_action :validate_request_security
  15. # 记录API访问日志
  16. after_action :log_api_access
  17. end
  18. private
  19. # 设置请求ID
  20. def set_request_id
  21. request_id = request.headers['X-Request-ID'] || SecureRandom.uuid
  22. response.headers['X-Request-ID'] = request_id
  23. @request_id = request_id
  24. end
  25. # 设置安全头
  26. def set_security_headers
  27. response.headers['X-Content-Type-Options'] = 'nosniff'
  28. response.headers['X-Frame-Options'] = 'DENY'
  29. response.headers['X-XSS-Protection'] = '1; mode=block'
  30. response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
  31. response.headers['Content-Security-Policy'] = "default-src 'self'"
  32. response.headers['X-API-Version'] = api_version
  33. response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' if sensitive_request?
  34. end
  35. # 检查API限流
  36. def check_rate_limits
  37. # IP限流检查
  38. rate_limiter = ApiRateLimitingService.check_ip_rate_limit(
  39. request.remote_ip,
  40. endpoint: request.path,
  41. request: request
  42. )
  43. unless rate_limiter.allowed?
  44. render_rate_limit_error(
  45. limit: rate_limiter.limit,
  46. remaining: rate_limiter.remaining_requests,
  47. reset_time: rate_limiter.reset_time,
  48. retry_after: calculate_retry_after(rate_limiter.reset_time)
  49. )
  50. return false
  51. end
  52. # 用户限流检查(如果已认证)
  53. if current_user
  54. user_rate_limiter = ApiRateLimitingService.check_user_rate_limit(
  55. current_user,
  56. endpoint: request.path,
  57. request: request
  58. )
  59. unless user_rate_limiter.allowed?
  60. render_rate_limit_error(
  61. limit: user_rate_limiter.limit,
  62. remaining: user_rate_limiter.remaining_requests,
  63. reset_time: user_rate_limiter.reset_time,
  64. retry_after: calculate_retry_after(user_rate_limiter.reset_time),
  65. scope: 'user'
  66. )
  67. return false
  68. end
  69. end
  70. # 全局限流检查
  71. global_rate_limiter = ApiRateLimitingService.check_global_rate_limit(
  72. endpoint: request.path,
  73. request: request
  74. )
  75. unless global_rate_limiter.allowed?
  76. render_rate_limit_error(
  77. limit: global_rate_limiter.limit,
  78. remaining: global_rate_limiter.remaining_requests,
  79. reset_time: global_rate_limiter.reset_time,
  80. retry_after: calculate_retry_after(global_rate_limiter.reset_time),
  81. scope: 'global'
  82. )
  83. return false
  84. end
  85. true
  86. end
  87. # 请求安全验证
  88. def validate_request_security
  89. # 检查User-Agent
  90. validate_user_agent
  91. # 检查请求大小
  92. validate_request_size
  93. # 检查可疑参数
  94. validate_suspicious_params
  95. # 检查请求频率模式
  96. validate_request_pattern
  97. end
  98. # 验证User-Agent
  99. def validate_user_agent
  100. user_agent = request.user_agent
  101. if user_agent.blank?
  102. render_error_response(
  103. error: '缺少User-Agent头',
  104. error_code: 'MISSING_USER_AGENT',
  105. error_type: 'security_error',
  106. status: :bad_request
  107. )
  108. return false
  109. end
  110. # 检查可疑的User-Agent模式
  111. suspicious_patterns = [
  112. /bot/i, /crawler/i, /spider/i,
  113. /scanner/i, /wget/i, /curl/i,
  114. /python/i, /java/i, /go-http/i
  115. ]
  116. if suspicious_patterns.any? { |pattern| user_agent.match?(pattern) } && !api_request?
  117. Rails.logger.warn "Suspicious User-Agent detected: #{user_agent}"
  118. end
  119. true
  120. end
  121. # 验证请求大小
  122. def validate_request_size
  123. content_length = request.content_length || 0
  124. max_size = max_request_size
  125. if content_length > max_size
  126. render_error_response(
  127. error: '请求体过大',
  128. error_code: 'REQUEST_TOO_LARGE',
  129. error_type: 'security_error',
  130. details: {
  131. max_size: "#{max_size / 1024 / 1024}MB",
  132. received_size: "#{content_length / 1024 / 1024}MB"
  133. },
  134. status: :payload_too_large
  135. )
  136. return false
  137. end
  138. true
  139. end
  140. # 验证可疑参数
  141. def validate_suspicious_params
  142. suspicious_patterns = [
  143. /<script/i, /javascript:/i, /vbscript:/i,
  144. /onload=/i, /onerror=/i, /onclick=/i,
  145. /union\s+select/i, /drop\s+table/i, /insert\s+into/i
  146. ]
  147. params.each do |key, value|
  148. next if value.is_a?(ActionController::Parameters)
  149. if value.is_a?(String) && suspicious_patterns.any? { |pattern| value.match?(pattern) }
  150. Rails.logger.warn "Suspicious parameter detected: #{key}=#{value[0..50]}"
  151. render_error_response(
  152. error: '请求包含可疑内容',
  153. error_code: 'SUSPICIOUS_CONTENT',
  154. error_type: 'security_error',
  155. status: :bad_request
  156. )
  157. return false
  158. end
  159. end
  160. true
  161. end
  162. # 验证请求模式
  163. def validate_request_pattern
  164. client_id = "#{request.remote_ip}:#{request.user_agent}"
  165. key = "request_pattern:#{Digest::MD5.hexdigest(client_id)}"
  166. # 获取最近请求时间
  167. recent_requests = Rails.cache.read(key) || []
  168. # 清理5分钟前的请求
  169. five_minutes_ago = 5.minutes.ago.to_f
  170. recent_requests.select! { |timestamp| timestamp > five_minutes_ago }
  171. # 检查是否存在异常请求模式
  172. if recent_requests.length > 50 # 5分钟内超过50个请求
  173. Rails.logger.warn "Suspicious request pattern detected: #{client_id}"
  174. render_error_response(
  175. error: '请求频率异常',
  176. error_code: 'SUSPICIOUS_PATTERN',
  177. error_type: 'security_error',
  178. details: {
  179. recent_requests: recent_requests.length,
  180. time_window: '5 minutes'
  181. },
  182. status: :too_many_requests
  183. )
  184. return false
  185. end
  186. # 记录当前请求时间
  187. recent_requests << Time.current.to_f
  188. Rails.cache.write(key, recent_requests, expires_in: 5.minutes)
  189. true
  190. end
  191. # 判断是否为敏感请求
  192. def sensitive_request?
  193. sensitive_endpoints = [
  194. /auth/, /login/, /register/, /password/,
  195. /admin/, /delete/, /update/, /create/
  196. ]
  197. sensitive_endpoints.any? { |pattern| request.path.match?(pattern) }
  198. end
  199. # 判断是否为API请求
  200. def api_request?
  201. request.path.start_with?('/api/')
  202. end
  203. # 获取最大请求大小
  204. def max_request_size
  205. case request.path
  206. when /upload/
  207. 100.megabytes
  208. when /auth/
  209. 1.megabyte
  210. else
  211. 10.megabytes
  212. end
  213. end
  214. # 计算重试时间
  215. def calculate_retry_after(reset_time)
  216. reset_timestamp = Time.parse(reset_time).to_i
  217. current_timestamp = Time.current.to_i
  218. [reset_timestamp - current_timestamp, 1].max
  219. end
  220. # 记录API访问日志
  221. def log_api_access
  222. log_data = {
  223. request_id: @request_id,
  224. ip: request.remote_ip,
  225. method: request.method,
  226. path: request.path,
  227. status: response.status,
  228. user_id: current_user&.id,
  229. user_agent: request.user_agent,
  230. duration: measure_request_duration,
  231. response_size: response.body&.size || 0
  232. }
  233. Rails.logger.info "API Access: #{log_data.to_json}"
  234. # 记录到专门的访问日志(如果配置了)
  235. if Rails.application.config.log_to_stdout
  236. Rails.logger.stdout.log_info("API_ACCESS", log_data)
  237. end
  238. end
  239. # 测量请求处理时间
  240. def measure_request_duration
  241. @request_start_time ||= Time.current
  242. ((Time.current - @request_start_time) * 1000).round(2)
  243. end
  244. # 渲染限流错误响应
  245. def render_rate_limit_error(limit:, remaining:, reset_time:, retry_after:, scope: nil)
  246. error_response = {
  247. success: false,
  248. error: 'API请求频率超过限制',
  249. error_code: 'RATE_LIMIT_EXCEEDED',
  250. error_type: 'rate_limit_error',
  251. timestamp: Time.current.iso8601,
  252. request_id: @request_id,
  253. details: {
  254. limit: limit,
  255. remaining: remaining,
  256. reset_time: reset_time,
  257. retry_after: retry_after,
  258. scope: scope
  259. },
  260. suggestions: [
  261. '请稍后重试',
  262. '如需更高限额,请联系管理员',
  263. '检查是否存在异常请求行为'
  264. ]
  265. }
  266. response.headers['Retry-After'] = retry_after.to_s
  267. response.headers['X-RateLimit-Limit'] = limit.to_s
  268. response.headers['X-RateLimit-Remaining'] = remaining.to_s
  269. response.headers['X-RateLimit-Reset'] = reset_time
  270. render json: error_response, status: :too_many_requests
  271. end
  272. # 渲染安全错误响应
  273. def render_security_error_response(message:, error_code:, details: {})
  274. error_response = {
  275. success: false,
  276. error: message,
  277. error_code: error_code,
  278. error_type: 'security_error',
  279. timestamp: Time.current.iso8601,
  280. request_id: @request_id,
  281. details: details,
  282. suggestions: [
  283. '请检查请求格式和内容',
  284. '确认请求来源可信',
  285. '如问题持续存在,请联系技术支持'
  286. ]
  287. }
  288. render json: error_response, status: :bad_request
  289. end
  290. # 生成API令牌(用于内部服务认证)
  291. def generate_api_token(service_name, expires_in: 1.hour)
  292. payload = {
  293. service: service_name,
  294. iat: Time.current.to_i,
  295. exp: (Time.current + expires_in).to_i,
  296. jti: SecureRandom.hex(16)
  297. }
  298. JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
  299. end
  300. # 验证API令牌
  301. def verify_api_token(token)
  302. decoded = JWT.decode(
  303. token,
  304. Rails.application.secrets.secret_key_base,
  305. true,
  306. { algorithm: 'HS256' }
  307. ).first
  308. # 检查服务是否在允许列表中
  309. allowed_services = Rails.application.config.x.allowed_api_services || []
  310. unless allowed_services.include?(decoded['service'])
  311. raise JWT::VerificationError, 'Service not allowed'
  312. end
  313. decoded
  314. rescue JWT::ExpiredSignature
  315. raise JWT::ExpiredSignature, 'Token expired'
  316. rescue JWT::DecodeError
  317. raise JWT::DecodeError, 'Invalid token'
  318. end
  319. end

app/controllers/concerns/api_versionable.rb

0.0% lines covered

151 relevant lines. 0 lines covered and 151 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApiVersionable - API版本控制模块
  3. # 为控制器提供API版本处理功能
  4. module ApiVersionable
  5. extend ActiveSupport::Concern
  6. included do
  7. before_action :set_api_version
  8. before_action :validate_api_version
  9. before_action :add_version_headers
  10. end
  11. private
  12. # 设置API版本
  13. def set_api_version
  14. @api_version = ApiVersionService.determine_api_version(request)
  15. Thread.current[:api_version] = @api_version
  16. end
  17. # 验证API版本
  18. def validate_api_version
  19. return if ApiVersionService.version_supported?(@api_version)
  20. # 版本不支持时返回错误
  21. render_error(
  22. message: "不支持的API版本: #{@api_version}",
  23. error_code: 'unsupported_api_version',
  24. details: {
  25. requested_version: @api_version,
  26. supported_versions: ApiVersionService::SUPPORTED_VERSIONS,
  27. recommended_version: ApiVersionService::DEFAULT_VERSION
  28. },
  29. status_code: 400
  30. )
  31. end
  32. # 添加版本相关的响应头
  33. def add_version_headers
  34. return unless response
  35. headers = ApiVersionService.create_version_headers(@api_version, response.headers)
  36. headers.each do |key, value|
  37. response.headers[key] = value
  38. end
  39. # 如果版本已弃用,添加弃用警告到响应中
  40. if ApiVersionService.version_deprecated?(@api_version)
  41. deprecation_warning = ApiVersionService.generate_deprecation_warning(@api_version)
  42. response.headers['X-API-Deprecation-Warning'] = deprecation_warning[:message]
  43. end
  44. end
  45. # 获取当前API版本
  46. # @return [String] 当前API版本
  47. def current_api_version
  48. @api_version || ApiVersionService::DEFAULT_VERSION
  49. end
  50. # 检查是否为特定版本
  51. # @param version [String] 要检查的版本
  52. # @return [Boolean] 是否为指定版本
  53. def api_version?(version)
  54. current_api_version == version
  55. end
  56. # 检查版本是否为v1
  57. # @return [Boolean] 是否为v1
  58. def api_v1?
  59. api_version?('v1')
  60. end
  61. # 检查版本是否已弃用
  62. # @return [Boolean] 是否已弃用
  63. def api_version_deprecated?
  64. ApiVersionService.version_deprecated?(current_api_version)
  65. end
  66. # 获取版本信息
  67. # @return [Hash] 版本信息
  68. def current_version_info
  69. ApiVersionService.version_info(current_api_version)
  70. end
  71. # 根据版本条件执行代码块
  72. # @yield 如果版本匹配,执行给定的代码块
  73. # @param version [String] 要匹配的版本
  74. def with_api_version(version)
  75. yield if api_version?(version)
  76. end
  77. # 版本条件渲染
  78. # @param v1_response [Proc] v1版本的响应
  79. # @param default_response [Proc] 默认响应
  80. def render_by_version(v1_response: nil, default_response: nil)
  81. case current_api_version
  82. when 'v1'
  83. v1_response&.call || default_response&.call
  84. else
  85. default_response&.call
  86. end
  87. end
  88. # 版本化的参数处理
  89. # @param params_hash [Hash] 不同版本的参数映射
  90. # @return [Hash] 处理后的参数
  91. def versioned_params(params_hash = {})
  92. case current_api_version
  93. when 'v1'
  94. params_hash[:v1] || {}
  95. else
  96. params_hash[:default] || {}
  97. end
  98. end
  99. # 版本化的序列化选项
  100. # @param options_hash [Hash] 不同版本的选项映射
  101. # @return [Hash] 序列化选项
  102. def versioned_serialize_options(options_hash = {})
  103. base_options = {
  104. current_user: current_user,
  105. api_version: current_api_version
  106. }
  107. version_options = case current_api_version
  108. when 'v1'
  109. options_hash[:v1] || {}
  110. else
  111. options_hash[:default] || {}
  112. end
  113. base_options.merge(version_options)
  114. end
  115. # 版本化的错误处理
  116. # @param error_hash [Hash] 不同版本的错误处理映射
  117. # @return [Hash] 错误响应
  118. def versioned_error_response(error_hash = {})
  119. base_error = {
  120. api_version: current_api_version,
  121. timestamp: Time.current.iso8601
  122. }
  123. version_error = case current_api_version
  124. when 'v1'
  125. error_hash[:v1] || {}
  126. else
  127. error_hash[:default] || {}
  128. end
  129. base_error.merge(version_error)
  130. end
  131. # 检查功能是否在当前版本中可用
  132. # @param feature [String, Symbol] 功能名称
  133. # @return [Boolean] 功能是否可用
  134. def feature_available?(feature)
  135. case current_api_version
  136. when 'v1'
  137. available_features = [
  138. :user_authentication,
  139. :reading_events,
  140. :check_ins,
  141. :flowers,
  142. :comments,
  143. :notifications,
  144. :analytics,
  145. :content_search,
  146. :content_export
  147. ]
  148. available_features.include?(feature.to_sym)
  149. else
  150. false
  151. end
  152. end
  153. # 如果功能不可用,返回功能不支持错误
  154. # @param feature [String, Symbol] 功能名称
  155. # @param message [String] 自定义错误消息
  156. def check_feature_availability!(feature, message = nil)
  157. return if feature_available?(feature)
  158. feature_name = feature.to_s.humanize
  159. error_message = message || "功能 '#{feature_name}' 在API版本 #{current_api_version} 中不可用"
  160. render_error(
  161. message: error_message,
  162. error_code: 'feature_not_available',
  163. details: {
  164. feature: feature,
  165. api_version: current_api_version,
  166. available_in_version: find_feature_version(feature)
  167. },
  168. status_code: 501
  169. )
  170. end
  171. private
  172. # 查找功能可用的版本
  173. # @param feature [String, Symbol] 功能名称
  174. # @return [String, nil] 可用的版本
  175. def find_feature_version(feature)
  176. ApiVersionService::SUPPORTED_VERSIONS.find do |version|
  177. case version
  178. when 'v1'
  179. available_features = [
  180. :user_authentication,
  181. :reading_events,
  182. :check_ins,
  183. :flowers,
  184. :comments,
  185. :notifications,
  186. :analytics,
  187. :content_search,
  188. :content_export
  189. ]
  190. available_features.include?(feature.to_sym)
  191. end
  192. end
  193. end
  194. end

app/controllers/concerns/authenticable.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. module Authenticable
  2. extend ActiveSupport::Concern
  3. included do
  4. before_action :authenticate_user!
  5. end
  6. private
  7. def authenticate_user!
  8. token = extract_token_from_header
  9. return render_unauthorized unless token
  10. decoded = User.decode_jwt_token(token)
  11. return render_unauthorized unless decoded
  12. @current_user = User.find_by(id: decoded[:user_id])
  13. render_unauthorized unless @current_user
  14. end
  15. def current_user
  16. @current_user
  17. end
  18. def extract_token_from_header
  19. header = request.headers["Authorization"]
  20. return nil unless header
  21. # 格式: "Bearer <token>"
  22. header.split(" ").last if header.start_with?("Bearer ")
  23. end
  24. def render_unauthorized
  25. render json: { error: "Unauthorized" }, status: :unauthorized
  26. end
  27. end

app/controllers/concerns/commentable.rb

0.0% lines covered

83 relevant lines. 0 lines covered and 83 lines missed.
    
  1. module Commentable
  2. extend ActiveSupport::Concern
  3. included do
  4. include ApiResponse
  5. before_action :authenticate_user!
  6. before_action :set_comment, only: [:update, :destroy]
  7. end
  8. # GET /api/comments
  9. def index
  10. @comments = fetch_comments.includes(:user).order(created_at: :asc)
  11. render_success(
  12. format_comments_response(@comments),
  13. message: '获取评论列表成功'
  14. )
  15. end
  16. # POST /api/comments
  17. def create
  18. @comment = build_comment(comment_params)
  19. @comment.user = current_user
  20. if @comment.save
  21. render_created(
  22. format_single_comment(@comment, true),
  23. message: '评论发布成功'
  24. )
  25. else
  26. render_error(
  27. '评论创建失败',
  28. errors: @comment.errors.full_messages
  29. )
  30. end
  31. end
  32. # PUT /api/comments/:id
  33. def update
  34. unless can_edit_comment?(@comment, current_user)
  35. return render_forbidden('无权限编辑此评论')
  36. end
  37. if @comment.update(comment_params)
  38. render_success(
  39. format_single_comment(@comment, true),
  40. message: '评论更新成功'
  41. )
  42. else
  43. render_error(
  44. '评论更新失败',
  45. errors: @comment.errors.full_messages
  46. )
  47. end
  48. end
  49. # DELETE /api/comments/:id
  50. def destroy
  51. unless can_edit_comment?(@comment, current_user)
  52. return render_forbidden('无权限删除此评论')
  53. end
  54. @comment.destroy
  55. render_success(
  56. nil,
  57. message: '评论删除成功'
  58. )
  59. end
  60. private
  61. def set_comment
  62. @comment = Comment.find(params[:id])
  63. rescue ActiveRecord::RecordNotFound
  64. render json: { error: '评论不存在' }, status: :not_found
  65. end
  66. def comment_params
  67. params.require(:comment).permit(:content)
  68. end
  69. # 抽象方法,由包含的类实现
  70. def fetch_comments
  71. raise NotImplementedError, "子类必须实现 fetch_comments 方法"
  72. end
  73. def build_comment(params)
  74. raise NotImplementedError, "子类必须实现 build_comment 方法"
  75. end
  76. # 格式化评论列表响应
  77. def format_comments_response(comments)
  78. comments.map { |comment|
  79. format_single_comment(comment)
  80. }
  81. end
  82. # 格式化单个评论
  83. def format_single_comment(comment, can_edit = false)
  84. comment.instance_variable_set(:@can_edit_current_user, can_edit || can_edit_comment?(comment, current_user))
  85. comment.send(:as_json)
  86. end
  87. # 权限检查 - 使用模型的公共方法
  88. def can_edit_comment?(comment, user)
  89. comment.send(:can_edit?, user)
  90. end
  91. end

app/controllers/concerns/global_error_handler.rb

0.0% lines covered

78 relevant lines. 0 lines covered and 78 lines missed.
    
  1. # frozen_string_literal: true
  2. # GlobalErrorHandler - 全局错误处理模块
  3. # 为所有控制器提供统一的错误处理机制
  4. module GlobalErrorHandler
  5. extend ActiveSupport::Concern
  6. included do
  7. # 全局异常处理
  8. rescue_from StandardError, with: :handle_standard_error
  9. rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found_error
  10. rescue_from ActiveRecord::RecordInvalid, with: :handle_validation_error
  11. rescue_from ActionController::ParameterMissing, with: :handle_parameter_missing_error
  12. rescue_from JWT::DecodeError, with: :handle_jwt_error
  13. rescue_from JWT::ExpiredSignature, with: :handle_jwt_expired_error
  14. rescue_from ActionController::BadRequest, with: :handle_bad_request_error
  15. rescue_from Timeout::Error, with: :handle_timeout_error
  16. rescue_from ActiveRecord::StatementInvalid, with: :handle_database_error
  17. end
  18. private
  19. def handle_standard_error(exception)
  20. handle_error(exception)
  21. end
  22. def handle_not_found_error(exception)
  23. handle_error(exception)
  24. end
  25. def handle_validation_error(exception)
  26. handle_error(exception)
  27. end
  28. def handle_parameter_missing_error(exception)
  29. handle_error(exception)
  30. end
  31. def handle_jwt_error(exception)
  32. handle_error(exception)
  33. end
  34. def handle_jwt_expired_error(exception)
  35. handle_error(exception)
  36. end
  37. def handle_bad_request_error(exception)
  38. handle_error(exception)
  39. end
  40. def handle_timeout_error(exception)
  41. handle_error(exception)
  42. end
  43. def handle_database_error(exception)
  44. handle_error(exception)
  45. end
  46. def handle_error(exception)
  47. # 使用全局错误处理服务
  48. error_handler = GlobalErrorHandlerService.handle_controller_exception(
  49. exception, self, action_name
  50. )
  51. # 记录错误信息
  52. log_error(error_handler)
  53. # 返回错误响应
  54. render_error_response(error_handler)
  55. end
  56. def log_error(error_handler)
  57. Rails.logger.error(
  58. "Controller Error: #{error_handler.error_code}",
  59. {
  60. controller: self.class.name,
  61. action: action_name,
  62. user_id: current_user&.id,
  63. error_details: error_handler.error_response
  64. }
  65. )
  66. end
  67. def render_error_response(error_handler)
  68. status = determine_http_status(error_handler.error_code)
  69. render json: error_handler.error_response, status: status
  70. end
  71. def determine_http_status(error_code)
  72. case error_code
  73. when 'RESOURCE_NOT_FOUND'
  74. :not_found
  75. when 'VALIDATION_ERROR', 'MISSING_PARAMETER', 'INVALID_PARAMETER'
  76. :unprocessable_entity
  77. when 'INVALID_TOKEN', 'TOKEN_EXPIRED'
  78. :unauthorized
  79. when 'TIMEOUT_ERROR', 'DATABASE_ERROR'
  80. :service_unavailable
  81. else
  82. :internal_server_error
  83. end
  84. end
  85. end

app/controllers/concerns/request_validator.rb

0.0% lines covered

313 relevant lines. 0 lines covered and 313 lines missed.
    
  1. # frozen_string_literal: true
  2. # RequestValidator - 请求验证模块
  3. # 提供统一的参数验证和请求安全检查
  4. module RequestValidator
  5. extend ActiveSupport::Concern
  6. # 验证必需参数
  7. def validate_required_params(*param_names)
  8. missing_params = param_names.select { |param| params[param].blank? }
  9. if missing_params.any?
  10. render_parameter_error_response(
  11. parameter: missing_params.join(', '),
  12. message: "缺少必需的参数: #{missing_params.join(', ')}"
  13. )
  14. return false
  15. end
  16. true
  17. end
  18. # 验证参数类型
  19. def validate_param_type(param_name, expected_type)
  20. return true if params[param_name].blank?
  21. case expected_type.to_s.downcase
  22. when 'string'
  23. unless params[param_name].is_a?(String)
  24. render_parameter_error_response(
  25. parameter: param_name,
  26. message: "参数 #{param_name} 必须是字符串类型"
  27. )
  28. return false
  29. end
  30. when 'integer'
  31. unless params[param_name].to_s.match?(/\A\d+\z/)
  32. render_parameter_error_response(
  33. parameter: param_name,
  34. message: "参数 #{param_name} 必须是整数类型"
  35. )
  36. return false
  37. end
  38. when 'float', 'decimal'
  39. unless params[param_name].to_s.match?(/\A\d+(\.\d+)?\z/)
  40. render_parameter_error_response(
  41. parameter: param_name,
  42. message: "参数 #{param_name} 必须是数字类型"
  43. )
  44. return false
  45. end
  46. when 'boolean'
  47. unless %w[true false 1 0].include?(params[param_name].to_s.downcase)
  48. render_parameter_error_response(
  49. parameter: param_name,
  50. message: "参数 #{param_name} 必须是布尔类型"
  51. )
  52. return false
  53. end
  54. when 'array'
  55. unless params[param_name].is_a?(Array)
  56. render_parameter_error_response(
  57. parameter: param_name,
  58. message: "参数 #{param_name} 必须是数组类型"
  59. )
  60. return false
  61. end
  62. when 'hash', 'object'
  63. unless params[param_name].is_a?(Hash) || params[param_name].is_a?(ActionController::Parameters)
  64. render_parameter_error_response(
  65. parameter: param_name,
  66. message: "参数 #{param_name} 必须是对象类型"
  67. )
  68. return false
  69. end
  70. end
  71. true
  72. end
  73. # 验证参数长度
  74. def validate_param_length(param_name, min_length: nil, max_length: nil)
  75. value = params[param_name]
  76. return true if value.blank?
  77. length = value.to_s.length
  78. if min_length && length < min_length
  79. render_parameter_error_response(
  80. parameter: param_name,
  81. message: "参数 #{param_name} 长度不能少于 #{min_length} 个字符"
  82. )
  83. return false
  84. end
  85. if max_length && length > max_length
  86. render_parameter_error_response(
  87. parameter: param_name,
  88. message: "参数 #{param_name} 长度不能超过 #{max_length} 个字符"
  89. )
  90. return false
  91. end
  92. true
  93. end
  94. # 验证数值范围
  95. def validate_param_range(param_name, min_value: nil, max_value: nil)
  96. value = params[param_name]
  97. return true if value.blank?
  98. numeric_value = value.to_f
  99. if min_value && numeric_value < min_value
  100. render_parameter_error_response(
  101. parameter: param_name,
  102. message: "参数 #{param_name} 不能小于 #{min_value}"
  103. )
  104. return false
  105. end
  106. if max_value && numeric_value > max_value
  107. render_parameter_error_response(
  108. parameter: param_name,
  109. message: "参数 #{param_name} 不能大于 #{max_value}"
  110. )
  111. return false
  112. end
  113. true
  114. end
  115. # 验证日期格式
  116. def validate_date_format(param_name, format: :iso8601)
  117. value = params[param_name]
  118. return true if value.blank?
  119. begin
  120. case format
  121. when :iso8601
  122. Date.iso8601(value.to_s)
  123. when :date
  124. Date.parse(value.to_s)
  125. when :datetime
  126. DateTime.parse(value.to_s)
  127. else
  128. Date.parse(value.to_s)
  129. end
  130. rescue ArgumentError
  131. render_parameter_error_response(
  132. parameter: param_name,
  133. message: "参数 #{param_name} 日期格式不正确,请使用 #{format} 格式"
  134. )
  135. return false
  136. end
  137. true
  138. end
  139. # 验证邮箱格式
  140. def validate_email_format(param_name)
  141. value = params[param_name]
  142. return true if value.blank?
  143. email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  144. unless value.match?(email_regex)
  145. render_parameter_error_response(
  146. parameter: param_name,
  147. message: "参数 #{param_name} 邮箱格式不正确"
  148. )
  149. return false
  150. end
  151. true
  152. end
  153. # 验证URL格式
  154. def validate_url_format(param_name)
  155. value = params[param_name]
  156. return true if value.blank?
  157. uri = URI.parse(value)
  158. unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
  159. render_parameter_error_response(
  160. parameter: param_name,
  161. message: "参数 #{param_name} URL格式不正确"
  162. )
  163. return false
  164. end
  165. true
  166. rescue URI::InvalidURIError
  167. render_parameter_error_response(
  168. parameter: param_name,
  169. message: "参数 #{param_name} URL格式不正确"
  170. )
  171. false
  172. end
  173. # 验证枚举值
  174. def validate_enum_values(param_name, allowed_values)
  175. value = params[param_name]
  176. return true if value.blank?
  177. unless allowed_values.include?(value)
  178. render_parameter_error_response(
  179. parameter: param_name,
  180. message: "参数 #{param_name} 必须是以下值之一: #{allowed_values.join(', ')}"
  181. )
  182. return false
  183. end
  184. true
  185. end
  186. # 验证分页参数
  187. def validate_pagination_params
  188. # 验证页码
  189. if params[:page].present?
  190. unless validate_param_type(:page, 'integer')
  191. return false
  192. end
  193. unless validate_param_range(:page, min_value: 1)
  194. return false
  195. end
  196. end
  197. # 验证每页数量
  198. if params[:per_page].present?
  199. unless validate_param_type(:per_page, 'integer')
  200. return false
  201. end
  202. unless validate_param_range(:per_page, min_value: 1, max_value: 100)
  203. return false
  204. end
  205. end
  206. # 设置默认值
  207. params[:page] ||= 1
  208. params[:per_page] ||= 20
  209. true
  210. end
  211. # 验证排序参数
  212. def validate_sort_params(allowed_fields = nil)
  213. if params[:sort_by].present?
  214. # 验证排序字段
  215. if allowed_fields && !allowed_values.include?(params[:sort_by])
  216. render_parameter_error_response(
  217. parameter: 'sort_by',
  218. message: "排序字段必须是以下值之一: #{allowed_fields.join(', ')}"
  219. )
  220. return false
  221. end
  222. # 验证排序方向
  223. if params[:sort_direction].present?
  224. unless validate_enum_values(:sort_direction, ['asc', 'desc'])
  225. return false
  226. end
  227. else
  228. params[:sort_direction] = 'desc'
  229. end
  230. end
  231. true
  232. end
  233. # 验证文件上传
  234. def validate_file_upload(param_name, max_size: nil, allowed_types: nil)
  235. file = params[param_name]
  236. return true if file.blank?
  237. # 验证文件大小
  238. if max_size && file.size > max_size
  239. render_parameter_error_response(
  240. parameter: param_name,
  241. message: "文件大小不能超过 #{max_size / 1024 / 1024}MB"
  242. )
  243. return false
  244. end
  245. # 验证文件类型
  246. if allowed_types && !allowed_types.include?(file.content_type)
  247. render_parameter_error_response(
  248. parameter: param_name,
  249. message: "文件类型必须是以下类型之一: #{allowed_types.join(', ')}"
  250. )
  251. return false
  252. end
  253. true
  254. end
  255. # 验证批量操作参数
  256. def validate_batch_operation_params
  257. unless validate_required_params(:ids)
  258. return false
  259. end
  260. unless validate_param_type(:ids, 'array')
  261. return false
  262. end
  263. if params[:ids].length > 100
  264. render_parameter_error_response(
  265. parameter: 'ids',
  266. message: "批量操作最多支持100个项目"
  267. )
  268. return false
  269. end
  270. true
  271. end
  272. # 验证JSON格式
  273. def validate_json_param(param_name)
  274. value = params[param_name]
  275. return true if value.blank?
  276. begin
  277. JSON.parse(value.to_s)
  278. rescue JSON::ParserError
  279. render_parameter_error_response(
  280. parameter: param_name,
  281. message: "参数 #{param_name} 必须是有效的JSON格式"
  282. )
  283. return false
  284. end
  285. true
  286. end
  287. # 验证时间范围
  288. def validate_time_range(start_param, end_param)
  289. if params[start_param].present? && params[end_param].present?
  290. unless validate_date_format(start_param) && validate_date_format(end_param)
  291. return false
  292. end
  293. start_time = params[start_param].to_s
  294. end_time = params[end_param].to_s
  295. if Time.parse(end_time) < Time.parse(start_time)
  296. render_parameter_error_response(
  297. parameter: end_param,
  298. message: "结束时间不能早于开始时间"
  299. )
  300. return false
  301. end
  302. end
  303. true
  304. end
  305. # 综合验证方法
  306. def validate_request_params(validations = {})
  307. validations.each do |param_name, options|
  308. # 验证必需参数
  309. if options[:required] && params[param_name].blank?
  310. render_parameter_error_response(
  311. parameter: param_name,
  312. message: "缺少必需的参数: #{param_name}"
  313. )
  314. return false
  315. end
  316. # 跳过空值的后续验证
  317. next if params[param_name].blank?
  318. # 验证类型
  319. if options[:type] && !validate_param_type(param_name, options[:type])
  320. return false
  321. end
  322. # 验证长度
  323. if options[:length] && !validate_param_length(param_name, **options[:length])
  324. return false
  325. end
  326. # 验证范围
  327. if options[:range] && !validate_param_range(param_name, **options[:range])
  328. return false
  329. end
  330. # 验证格式
  331. if options[:format] == :email && !validate_email_format(param_name)
  332. return false
  333. elsif options[:format] == :url && !validate_url_format(param_name)
  334. return false
  335. elsif options[:format] == :json && !validate_json_param(param_name)
  336. return false
  337. end
  338. # 验证枚举值
  339. if options[:in] && !validate_enum_values(param_name, options[:in])
  340. return false
  341. end
  342. end
  343. true
  344. end
  345. end

app/controllers/concerns/user_experience_enhancer.rb

0.0% lines covered

236 relevant lines. 0 lines covered and 236 lines missed.
    
  1. # frozen_string_literal: true
  2. # UserExperienceEnhancer - 用户体验增强模块
  3. # 为控制器添加用户体验增强功能
  4. module UserExperienceEnhancer
  5. extend ActiveSupport::Concern
  6. # 在响应中添加用户体验增强数据
  7. def enhance_response_with_user_experience(response_data = nil)
  8. return response_data unless current_user
  9. enhanced_data = response_data || {}
  10. user_experience_data = build_user_experience_data
  11. # 将用户体验数据添加到响应中
  12. if enhanced_data.is_a?(Hash)
  13. enhanced_data[:user_experience] = user_experience_data
  14. else
  15. # 如果响应数据不是Hash,包装它
  16. enhanced_data = {
  17. data: enhanced_data,
  18. user_experience: user_experience_data
  19. }
  20. end
  21. enhanced_data
  22. end
  23. private
  24. def build_user_experience_data
  25. enhancer_service = UserExperienceEnhancerService.new(
  26. user: current_user,
  27. request_context: build_request_context,
  28. enhancement_options: enhancement_options
  29. )
  30. enhancer_service.call
  31. {
  32. recommendations: enhancer_service.recommendations,
  33. personalization: enhancer_service.personalization_data,
  34. quick_actions: enhancer_service.send(:generate_quick_actions),
  35. contextual_tips: enhancer_service.send(:generate_contextual_tips),
  36. preferences: build_preferences_data,
  37. notifications: build_notifications_data
  38. }
  39. end
  40. def build_request_context
  41. {
  42. action: action_name,
  43. controller: controller_name,
  44. current_page: detect_current_page,
  45. user_agent: request.user_agent,
  46. timestamp: Time.current,
  47. parameters: filtered_parameters
  48. }
  49. end
  50. def enhancement_options
  51. {
  52. include_recommendations: should_include_recommendations?,
  53. include_personalization: should_include_personalization?,
  54. include_quick_actions: should_include_quick_actions?,
  55. include_tips: should_include_tips?
  56. }
  57. end
  58. def should_include_recommendations?
  59. # 在主页、列表页面显示推荐
  60. %w[index show].include?(action_name) && !request.format.json?
  61. end
  62. def should_include_personalization?
  63. # 为认证用户显示个性化内容
  64. current_user.present?
  65. end
  66. def should_include_quick_actions?
  67. # 在所有页面显示快捷操作
  68. current_user.present?
  69. end
  70. def should_include_tips?
  71. # 根据时间和用户行为显示提示
  72. contextual_tips_enabled?
  73. end
  74. def contextual_tips_enabled?
  75. # 可以基于用户设置或系统配置
  76. current_user&.preferences&.dig('contextual_tips_enabled') != false
  77. end
  78. def detect_current_page
  79. case controller_name
  80. when 'posts'
  81. 'posts'
  82. when 'reading_events'
  83. 'events'
  84. when 'users'
  85. 'profile'
  86. when 'notifications'
  87. 'notifications'
  88. else
  89. 'other'
  90. end
  91. end
  92. def build_preferences_data
  93. return {} unless current_user
  94. {
  95. theme: current_user.preferences&.dig('theme') || 'light',
  96. language: current_user.preferences&.dig('language') || 'zh-CN',
  97. notifications_enabled: current_user.preferences&.dig('notifications_enabled') != false,
  98. auto_refresh: current_user.preferences&.dig('auto_refresh') || false
  99. }
  100. end
  101. def build_notifications_data
  102. return {} unless current_user
  103. unread_count = current_user.notifications.where(read: false).count
  104. recent_notifications = current_user.notifications
  105. .order(created_at: :desc)
  106. .limit(5)
  107. {
  108. unread_count: unread_count,
  109. recent_count: recent_notifications.count,
  110. has_new_notifications: unread_count > 0,
  111. notification_types: get_notification_types(recent_notifications)
  112. }
  113. end
  114. def get_notification_types(notifications)
  115. types = notifications.pluck(:notification_type)
  116. types.group_by(&:itself).transform_values(&:count)
  117. end
  118. def filtered_parameters
  119. # 过滤敏感参数
  120. allowed_params = %w[page per_page sort_by sort_direction category status]
  121. params.to_h.select { |key, _| allowed_params.include?(key.to_s) }
  122. end
  123. # 重写渲染方法以自动添加用户体验增强
  124. def render_success_response(data: nil, message: 'Success', meta: {})
  125. enhanced_data = enhance_response_with_user_experience(data)
  126. super(data: enhanced_data, message: message, meta: meta)
  127. end
  128. def render_paginated_response(data:, pagination:, message: 'Success', meta: {})
  129. enhanced_data = enhance_response_with_user_experience(data)
  130. super(data: enhanced_data, pagination: pagination, message: message, meta: meta)
  131. end
  132. # 渲染增强的用户体验响应
  133. def render_enhanced_response(data: nil, message: 'Success', status: :ok)
  134. enhanced_response = {
  135. success: true,
  136. message: message,
  137. data: enhance_response_with_user_experience(data),
  138. timestamp: Time.current.iso8601,
  139. request_id: @request_id
  140. }
  141. render json: enhanced_response, status: status
  142. end
  143. # 添加用户行为追踪
  144. def track_user_action(action_type, details = {})
  145. return unless current_user
  146. UserActivityTracker.track(
  147. user: current_user,
  148. action_type: action_type,
  149. details: details.merge(
  150. controller: controller_name,
  151. action: action_name,
  152. ip: request.remote_ip,
  153. user_agent: request.user_agent
  154. )
  155. )
  156. end
  157. # 记录用户偏好
  158. def record_user_preference(preference_key, value)
  159. return unless current_user
  160. preferences = current_user.preferences || {}
  161. preferences[preference_key] = value
  162. current_user.update(preferences: preferences)
  163. end
  164. # 获取用户最近活动
  165. def get_recent_user_activities(limit = 5)
  166. return [] unless current_user
  167. UserActivity.where(user: current_user)
  168. .order(created_at: :desc)
  169. .limit(limit)
  170. end
  171. # 检查用户是否为新用户
  172. def new_user?
  173. return false unless current_user
  174. current_user.created_at > 7.days.ago
  175. end
  176. # 检查用户是否需要引导
  177. def needs_onboarding?
  178. return false unless current_user
  179. # 新用户且完成度低
  180. new_user? && user_completion_percentage < 50
  181. end
  182. def user_completion_percentage
  183. return 0 unless current_user
  184. completion_items = [
  185. current_user.nickname.present?,
  186. current_user.avatar.present?,
  187. current_user.posts.count > 0,
  188. current_user.comments.count > 0,
  189. current_user.event_enrollments.count > 0
  190. ]
  191. (completion_items.count(true) * 100 / completion_items.length).round
  192. end
  193. # 检查用户是否需要鼓励
  194. def needs_encouragement?
  195. return false unless current_user
  196. # 长时间未活跃的用户
  197. last_activity = current_user.posts.maximum(:created_at) || current_user.created_at
  198. last_activity < 7.days.ago
  199. end
  200. # 生成鼓励消息
  201. def generate_encouragement_message
  202. return nil unless needs_encouragement?
  203. messages = [
  204. "好久不见,想念您的分享!",
  205. "新的精彩内容等您发现",
  206. "朋友们都很想念您的参与",
  207. "分享您的读书心得吧"
  208. ]
  209. messages.sample
  210. end
  211. # 添加个性化响应头
  212. def add_personalization_headers
  213. return unless current_user
  214. response.headers['X-User-Level'] = calculate_user_level.to_s
  215. response.headers['X-New-User'] = new_user?.to_s
  216. response.headers['X-Needs-Onboarding'] = needs_onboarding?.to_s
  217. response.headers['X-User-Timezone'] = current_user.preferences&.dig('timezone') || 'Asia/Shanghai'
  218. end
  219. def calculate_user_level
  220. # 简化的用户等级计算
  221. score = 0
  222. score += (current_user.posts.count * 10)
  223. score += (current_user.comments.count * 5)
  224. score += (current_user.event_enrollments.count * 15)
  225. case score
  226. when 0..50
  227. 1
  228. when 51..200
  229. 2
  230. when 201..500
  231. 3
  232. when 501..1000
  233. 4
  234. else
  235. 5
  236. end
  237. end
  238. # 检查并设置用户偏好
  239. def set_user_preferences_if_needed
  240. return unless current_user
  241. # 如果用户没有偏好设置,设置默认值
  242. if current_user.preferences.blank?
  243. default_preferences = {
  244. 'theme' => 'light',
  245. 'language' => 'zh-CN',
  246. 'timezone' => 'Asia/Shanghai',
  247. 'notifications_enabled' => true,
  248. 'contextual_tips_enabled' => true,
  249. 'auto_refresh' => false
  250. }
  251. current_user.update(preferences: default_preferences)
  252. end
  253. end
  254. # 在每个请求开始时调用
  255. def enhance_user_request
  256. return unless current_user
  257. # 设置用户偏好
  258. set_user_preferences_if_needed
  259. # 添加个性化响应头
  260. add_personalization_headers
  261. # 追踪用户活动
  262. track_user_action("page_view", {
  263. path: request.path,
  264. method: request.method
  265. })
  266. end
  267. end

app/jobs/application_job.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class ApplicationJob < ActiveJob::Base
  2. # Automatically retry jobs that encountered a deadlock
  3. # retry_on ActiveRecord::Deadlocked
  4. # Most jobs are safe to ignore if the underlying records are no longer available
  5. # discard_on ActiveJob::DeserializationError
  6. end

app/mailers/application_mailer.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class ApplicationMailer < ActionMailer::Base
  2. default from: "from@example.com"
  3. layout "mailer"
  4. end

app/models/application_record.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class ApplicationRecord < ActiveRecord::Base
  2. 1 primary_abstract_class
  3. end

app/models/check_in.rb

40.49% lines covered

163 relevant lines. 66 lines covered and 97 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: check_ins
  4. #
  5. # id :integer not null, primary key
  6. # user_id :integer not null
  7. # reading_schedule_id :integer not null
  8. # enrollment_id :integer not null
  9. # content :text not null
  10. # word_count :integer default(0), not null
  11. # status :integer default(0), not null
  12. # submitted_at :datetime not null
  13. # created_at :datetime not null
  14. # updated_at :datetime not null
  15. #
  16. # Indexes
  17. #
  18. # index_check_ins_on_reading_schedule_id (reading_schedule_id)
  19. # index_check_ins_on_submitted_at (submitted_at)
  20. # index_check_ins_on_user_id (user_id)
  21. # index_check_ins_on_user_id_and_reading_schedule_id (user_id, reading_schedule_id) UNIQUE
  22. #
  23. # Foreign Keys
  24. #
  25. # fk_rails_... (enrollment_id => event_enrollments.id)
  26. # fk_rails_... (reading_schedule_id => reading_schedules.id)
  27. # fk_rails_... (user_id => users.id)
  28. #
  29. 1 class CheckIn < ApplicationRecord
  30. # 打卡状态枚举
  31. 1 enum :status, {
  32. normal: 0, # 正常打卡
  33. supplement: 1, # 补卡
  34. late: 2 # 迟到
  35. }, default: :normal
  36. # 关联关系
  37. 1 belongs_to :user
  38. 1 belongs_to :reading_schedule
  39. 1 belongs_to :enrollment, class_name: 'EventEnrollment'
  40. 1 has_many :flowers, dependent: :destroy
  41. 1 has_one :reading_event, through: :reading_schedule
  42. 1 has_many :comments, as: :commentable, dependent: :destroy
  43. # 验证规则
  44. 1 validates :content, presence: true, length: { minimum: 50, maximum: 2000 }
  45. 1 validates :word_count, numericality: { greater_than_or_equal_to: 0 }
  46. 1 validates :user_id, uniqueness: { scope: :reading_schedule_id, message: "今日已打卡" }
  47. 1 validate :must_be_active_participant
  48. 1 validate :schedule_within_activity_period
  49. 1 validate :content_word_count_limit
  50. 1 validate :cannot_check_in_after_deadline, on: :create
  51. # 回调
  52. 1 before_validation :calculate_word_count, if: :content_changed?
  53. 1 before_validation :set_status, on: :create
  54. 1 before_create :set_submitted_at
  55. 1 after_create :update_enrollment_stats
  56. 1 after_create :notify_check_in_submitted
  57. 1 after_destroy :rollback_enrollment_stats
  58. # 作用域
  59. 1 scope :today, -> { joins(:reading_schedule).where(reading_schedules: { date: Date.current }) }
  60. 1 scope :normal, -> { where(status: :normal) }
  61. 1 scope :supplement, -> { where(status: :supplement) }
  62. 1 scope :late, -> { where(status: :late) }
  63. 1 scope :recent, -> { order(submitted_at: :desc) }
  64. 1 scope :by_word_count, ->(direction = :desc) { order(word_count: direction) }
  65. # 委托方法
  66. 1 delegate :title, to: :reading_event, prefix: true
  67. 1 delegate :nickname, to: :user, prefix: true
  68. 1 delegate :date, to: :reading_schedule, prefix: true
  69. # 状态方法
  70. 1 def today?
  71. reading_schedule.today?
  72. end
  73. 1 def on_time?
  74. return true if status == 'normal'
  75. false
  76. end
  77. 1 def is_supplement?
  78. status == 'supplement'
  79. end
  80. 1 def is_late?
  81. status == 'late'
  82. end
  83. 1 def can_be_edited?
  84. # 活动结束后不能编辑
  85. return false unless reading_event
  86. reading_event.end_date >= Date.current
  87. end
  88. 1 def can_receive_flowers?
  89. flowers_count < 3 # 每个打卡最多3朵小红花
  90. end
  91. 1 def can_be_deleted?
  92. # 活动结束后不能删除
  93. return false unless reading_event
  94. reading_event.end_date >= Date.current
  95. end
  96. # 统计方法
  97. 1 def flowers_count
  98. flowers.count
  99. end
  100. 1 def total_flowers_received
  101. flowers.sum(&:amount) if flowers.respond_to?(:sum)
  102. end
  103. 1 def engagement_score
  104. # 计算参与度分数:字数分数 + 小红花分数
  105. word_score = [word_count / 100.0, 10.0].min # 最多10分
  106. flower_score = flowers_count * 2.0 # 每朵小红花2分
  107. (word_score + flower_score).round(2)
  108. end
  109. # 小红花相关方法
  110. 1 def give_flower!(giver, comment = nil)
  111. return false unless can_receive_flowers?
  112. return false if giver == user # 不能给自己发小红花
  113. # 检查发放权限
  114. unless reading_event.can_give_flowers?(giver, reading_schedule)
  115. return false
  116. end
  117. transaction do
  118. flower = flowers.create!(
  119. giver: giver,
  120. recipient: user,
  121. comment: comment,
  122. reading_schedule: reading_schedule
  123. )
  124. # 更新接收者的统计
  125. enrollment.increment!(:flowers_received_count)
  126. # 发送通知
  127. notify_flower_given(flower)
  128. end
  129. true
  130. end
  131. # 内容方法
  132. 1 def content_preview(length = 100)
  133. content.truncate(length)
  134. end
  135. 1 def reading_time_estimate
  136. # 基于字数估算阅读时间(假设每分钟200字)
  137. (word_count / 200.0).ceil
  138. end
  139. # 格式化内容
  140. 1 def formatted_content(options = {})
  141. ContentFormatterService.format(content, options)
  142. end
  143. # 内容摘要
  144. 1 def content_summary(max_length = 200)
  145. ContentFormatterService.generate_summary(content, max_length)
  146. end
  147. # 提取关键词
  148. 1 def keywords(max_count = 5)
  149. ContentFormatterService.extract_keywords(content, max_count)
  150. end
  151. # 内容质量分数
  152. 1 def quality_score
  153. ContentFormatterService.calculate_quality_score(content)
  154. end
  155. # 内容合规性检查
  156. 1 def compliance_check
  157. ContentFormatterService.check_compliance(content)
  158. end
  159. # 是否为高质量内容
  160. 1 def high_quality?
  161. quality_score >= 50
  162. end
  163. # 是否有格式问题
  164. 1 def has_formatting_issues?
  165. check = compliance_check
  166. check[:issues].any? { |issue| issue[:type] == 'poor_formatting' }
  167. end
  168. # 是否包含敏感词
  169. 1 def contains_sensitive_words?
  170. check = compliance_check
  171. check[:issues].any? { |issue| issue[:type] == 'sensitive_words' }
  172. end
  173. # API响应格式化
  174. 1 def as_json_for_api(options = {})
  175. base_data = {
  176. id: id,
  177. content: content,
  178. word_count: word_count,
  179. status: status,
  180. submitted_at: submitted_at,
  181. created_at: created_at,
  182. updated_at: updated_at,
  183. engagement_score: engagement_score,
  184. quality_score: quality_score,
  185. high_quality: high_quality?
  186. }
  187. # 可选包含关联数据
  188. if options[:include_user]
  189. base_data[:user] = user.as_json_for_api
  190. end
  191. if options[:include_reading_schedule]
  192. base_data[:reading_schedule] = {
  193. id: reading_schedule.id,
  194. day_number: reading_schedule.day_number,
  195. date: reading_schedule.date,
  196. reading_progress: reading_schedule.reading_progress
  197. }
  198. end
  199. if options[:include_reading_event]
  200. base_data[:reading_event] = {
  201. id: reading_event.id,
  202. title: reading_event.title,
  203. book_name: reading_event.book_name
  204. }
  205. end
  206. if options[:include_flowers]
  207. base_data[:flowers] = flowers.map do |flower|
  208. {
  209. id: flower.id,
  210. amount: flower.amount,
  211. flower_type: flower.flower_type,
  212. comment: flower.comment,
  213. giver: flower.giver.as_json_for_api,
  214. created_at: flower.created_at
  215. }
  216. end
  217. base_data[:flowers_count] = flowers_count
  218. base_data[:total_flowers_received] = total_flowers_received
  219. end
  220. if options[:include_comments]
  221. base_data[:comments] = comments.map(&:as_json_for_api)
  222. base_data[:comments_count] = comments.count
  223. end
  224. # 内容相关
  225. if options[:include_content_analysis]
  226. base_data[:content_preview] = content_preview(options[:preview_length] || 100)
  227. base_data[:reading_time_estimate] = reading_time_estimate
  228. base_data[:keywords] = keywords
  229. base_data[:content_summary] = content_summary
  230. end
  231. base_data
  232. end
  233. 1 private
  234. # 验证方法
  235. 1 def must_be_active_participant
  236. return unless enrollment && reading_event
  237. # 直接检查报名状态和类型,避免调用私有方法
  238. unless enrollment.enrolled? && enrollment.participant?
  239. errors.add(:base, "您不是该活动的有效参与者")
  240. end
  241. end
  242. 1 def schedule_within_activity_period
  243. return unless reading_schedule && reading_event
  244. schedule_date = reading_schedule.date
  245. unless schedule_date.between?(reading_event.start_date, reading_event.end_date)
  246. errors.add(:base, "打卡日期不在活动期间内")
  247. end
  248. end
  249. 1 def content_word_count_limit
  250. return unless content
  251. if word_count < 50
  252. errors.add(:content, "内容太短,至少需要50个字")
  253. elsif word_count > 2000
  254. errors.add(:content, "内容太长,最多2000个字")
  255. end
  256. end
  257. 1 def cannot_check_in_after_deadline
  258. return unless reading_schedule
  259. # 当天的打卡可以在晚上11:59前提交
  260. schedule_date = reading_schedule.date
  261. deadline = schedule_date.to_time.end_of_day
  262. if Time.current > deadline && status == 'normal'
  263. errors.add(:base, "打卡时间已过,只能补卡")
  264. end
  265. end
  266. # 回调方法
  267. 1 def calculate_word_count
  268. self.word_count = content.to_s.strip.length
  269. end
  270. 1 def set_status
  271. schedule_date = reading_schedule.date
  272. current_time = Time.current
  273. if schedule_date == Date.current
  274. self.status = 'normal'
  275. elsif schedule_date < Date.current
  276. self.status = 'supplement'
  277. elsif current_time > schedule_date.to_time.end_of_day
  278. self.status = 'late'
  279. end
  280. end
  281. 1 def set_submitted_at
  282. self.submitted_at ||= Time.current
  283. end
  284. 1 def update_enrollment_stats
  285. return unless enrollment
  286. enrollment.increment!(:check_ins_count)
  287. enrollment.update_completion_rate!
  288. end
  289. 1 def rollback_enrollment_stats
  290. return unless enrollment
  291. # 减少打卡次数
  292. enrollment.decrement!(:check_ins_count)
  293. # 减少小红花数量(如果有)
  294. flowers_count = flowers.count
  295. if flowers_count > 0
  296. enrollment.decrement!(:flowers_received_count, flowers_count)
  297. end
  298. # 重新计算完成率
  299. enrollment.update_completion_rate!
  300. end
  301. # 通知方法
  302. 1 def notify_check_in_submitted
  303. # 发送打卡提交通知
  304. CheckInNotificationService.notify_submitted(self)
  305. end
  306. 1 def notify_flower_given(flower)
  307. # 发送小红花通知
  308. FlowerNotificationService.notify_given(flower)
  309. end
  310. # 是否获得小红花
  311. 1 def has_flower?
  312. flower.present?
  313. end
  314. # 是否可以补卡
  315. 1 def can_makeup?
  316. reading_schedule.date < Date.today &&
  317. reading_event.in_progress?
  318. end
  319. end

app/models/comment.rb

31.67% lines covered

60 relevant lines. 19 lines covered and 41 lines missed.
    
  1. 1 class Comment < ApplicationRecord
  2. 1 belongs_to :post, optional: true
  3. 1 belongs_to :user
  4. 1 belongs_to :commentable, polymorphic: true
  5. # 验证
  6. 1 validates :content, presence: true, length: { minimum: 2, maximum: 1000 }
  7. 1 validates :commentable, presence: true, if: :should_validate_commentable?
  8. 1 private
  9. 1 def should_validate_commentable?
  10. 34 commentable_type != 'CheckIn'
  11. end
  12. # 权限检查方法 - 改为公共方法
  13. 1 def can_edit?(current_user)
  14. return false unless current_user
  15. return true if current_user.any_admin? # 管理员可以编辑任何评论
  16. return true if user_id == current_user.id # 作者可以编辑自己的评论
  17. false
  18. end
  19. # 时间格式化
  20. 1 def time_ago
  21. seconds = Time.current - created_at
  22. minutes = seconds / 60
  23. hours = minutes / 60
  24. days = hours / 24
  25. if days >= 1
  26. "#{days.to_i}天前"
  27. elsif hours >= 1
  28. "#{hours.to_i}小时前"
  29. elsif minutes >= 1
  30. "#{minutes.to_i}分钟前"
  31. else
  32. "刚刚"
  33. end
  34. end
  35. # API序列化方法 - 标准化API响应格式
  36. 1 def as_json_for_api(options = {})
  37. current_user = options[:current_user]
  38. result = {
  39. id: id,
  40. content: content,
  41. created_at: created_at,
  42. updated_at: updated_at,
  43. time_ago: time_ago,
  44. author: user.as_json_for_api
  45. }
  46. # 添加评论对象信息
  47. if commentable
  48. result[:commentable] = {
  49. type: commentable_type,
  50. id: commentable_id,
  51. title: commentable_title
  52. }
  53. end
  54. # 添加当前用户的权限信息
  55. if current_user
  56. result[:interactions] = {
  57. can_edit: can_edit?(current_user)
  58. }
  59. end
  60. # 包含回复评论
  61. if options[:include_replies]
  62. result[:replies] = replies.limit(5).map { |reply| reply.as_json_for_api(options) }
  63. end
  64. result
  65. end
  66. # JSON 序列化方法 - 保持向后兼容
  67. 1 def as_json(options = {})
  68. json_hash = {
  69. id: id,
  70. content: content,
  71. created_at: created_at,
  72. updated_at: updated_at,
  73. author_info: author_info,
  74. time_ago: time_ago,
  75. can_edit_current_user: @can_edit_current_user || false
  76. }
  77. # 如果有关联的用户信息,包含用户数据
  78. if associated_user_loaded?
  79. json_hash[:user] = {
  80. id: user.id,
  81. nickname: user.nickname,
  82. avatar_url: user.avatar_url
  83. }
  84. end
  85. json_hash
  86. end
  87. # 设置当前用户是否可编辑的权限 - 改为公共方法
  88. 1 def can_edit_current_user=(value)
  89. @can_edit_current_user = value
  90. end
  91. # 检查用户数据是否已预加载 - 改为公共方法
  92. 1 def associated_user_loaded?
  93. loaded_associations = association(:user).loaded?
  94. loaded_associations
  95. rescue
  96. false
  97. end
  98. # 获取评论对象的标题
  99. 1 def commentable_title
  100. return unless commentable
  101. case commentable_type
  102. when 'Post'
  103. commentable.title
  104. when 'CheckIn'
  105. "第#{commentable.day_number}天打卡"
  106. when 'ReadingEvent'
  107. commentable.title
  108. when 'Flower'
  109. "小红花 #{commentable.id}"
  110. else
  111. commentable_type
  112. end
  113. end
  114. # 获取回复评论
  115. 1 def replies
  116. Comment.where(commentable_type: 'Comment', commentable_id: id)
  117. end
  118. 1 private
  119. 1 def author_info
  120. {
  121. id: user.id,
  122. nickname: user.nickname,
  123. avatar_url: user.avatar_url,
  124. role: user.role_display_name
  125. }
  126. end
  127. end

app/models/content_report.rb

0.0% lines covered

106 relevant lines. 0 lines covered and 106 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: content_reports
  4. #
  5. # id :integer not null, primary key
  6. # user_id :integer not null, foreign_key
  7. # check_in_id :integer not null, foreign_key
  8. # admin_id :integer foreign_key
  9. # reason :enum default("other"), not null
  10. # description :text
  11. # status :enum default("pending"), not null
  12. # admin_notes :text
  13. # reviewed_at :datetime
  14. # created_at :datetime not null
  15. # updated_at :datetime not null
  16. #
  17. # Indexes
  18. #
  19. # index_content_reports_on_check_in_id (check_in_id)
  20. # index_content_reports_on_created_at (created_at)
  21. # index_content_reports_on_reason (reason)
  22. # index_content_reports_on_status (status)
  23. # index_content_reports_on_user_id (user_id)
  24. # index_content_reports_unique_reporting (user_id, check_in_id) UNIQUE
  25. #
  26. # Foreign Keys
  27. #
  28. # fk_rails_... (check_in_id => check_ins.id)
  29. # fk_rails_... (user_id => users.id)
  30. #
  31. class ContentReport < ApplicationRecord
  32. # 举报原因枚举
  33. enum :reason, {
  34. sensitive_words: '敏感词',
  35. inappropriate_content: '不当内容',
  36. spam: '垃圾内容',
  37. other: '其他'
  38. }, default: :other
  39. # 处理状态枚举
  40. enum :status, {
  41. pending: '待处理',
  42. reviewed: '已查看',
  43. dismissed: '已忽略',
  44. action_taken: '已处理'
  45. }, default: :pending
  46. # 关联关系
  47. belongs_to :user
  48. belongs_to :check_in
  49. belongs_to :admin, class_name: 'User', optional: true
  50. # 验证规则
  51. validates :user_id, uniqueness: { scope: :check_in_id, message: '您已经举报过此内容' }
  52. validates :description, length: { maximum: 500 }
  53. validate :cannot_report_own_content
  54. # 回调
  55. after_create :notify_admins
  56. after_update :send_status_update_notification
  57. # 作用域
  58. scope :pending, -> { where(status: :pending) }
  59. scope :reviewed, -> { where.not(status: :pending) }
  60. scope :by_reason, ->(reason) { where(reason: reason) }
  61. scope :recent, -> { order(created_at: :desc) }
  62. # 委托方法
  63. delegate :content, to: :check_in, prefix: true
  64. delegate :nickname, to: :user, prefix: true
  65. delegate :created_at, to: :check_in, prefix: true
  66. # 状态方法
  67. def pending?
  68. status == 'pending'
  69. end
  70. def reviewed?
  71. reviewed_at.present?
  72. end
  73. def processed?
  74. %w[reviewed dismissed action_taken].include?(status.to_s)
  75. end
  76. def action_taken?
  77. status == 'action_taken'
  78. end
  79. # 操作方法
  80. def review!(admin:, notes: nil, action: :reviewed)
  81. return false unless admin.can_approve_events?
  82. transaction do
  83. update!(
  84. admin: admin,
  85. admin_notes: notes,
  86. status: action,
  87. reviewed_at: Time.current
  88. )
  89. # 根据处理结果执行相应操作
  90. case action.to_sym
  91. when :action_taken
  92. handle_content_action
  93. end
  94. log_review_action(admin, action)
  95. end
  96. true
  97. rescue => e
  98. Rails.logger.error "Content report review failed: #{e.message}"
  99. false
  100. end
  101. def dismiss!(admin:, notes: nil)
  102. review!(admin: admin, notes: notes, action: :dismissed)
  103. end
  104. # 类方法
  105. def self.statistics(days = 30)
  106. start_date = days.days.ago.to_date
  107. {
  108. total_reports: where('created_at >= ?', start_date).count,
  109. pending_reports: pending.where('created_at >= ?', start_date).count,
  110. by_reason: where('created_at >= ?', start_date).group(:reason).count,
  111. by_status: where('created_at >= ?', start_date).group(:status).count,
  112. daily_trends: where('created_at >= ?', start_date)
  113. .group('DATE(created_at)')
  114. .count
  115. }
  116. end
  117. def self.high_priority_reports
  118. # 需要优先处理的举报
  119. pending.joins(:check_in)
  120. .where('check_ins.created_at < ?', 1.hour.ago)
  121. .or(where(reason: :sensitive_words))
  122. end
  123. private
  124. # 验证方法
  125. def cannot_report_own_content
  126. if user_id == check_in.user_id
  127. errors.add(:base, '不能举报自己的内容')
  128. end
  129. end
  130. # 回调方法
  131. def notify_admins
  132. # 通知管理员有新的举报
  133. return unless Rails.env.production?
  134. # 这里可以实现邮件、短信或推送通知
  135. ContentModerationService.notify_admins_of_new_report(self)
  136. end
  137. def send_status_update_notification
  138. # 向举报人发送状态更新通知
  139. return unless saved_change_to_status?
  140. ContentModerationService.notify_reporter_of_status_change(self)
  141. end
  142. def handle_content_action
  143. # 处理内容(如隐藏、删除等)
  144. case reason.to_sym
  145. when :sensitive_words
  146. # 可以隐藏包含敏感词的内容
  147. check_in.update!(status: :hidden) if check_in.respond_to?(:status=)
  148. when :spam
  149. # 可以删除垃圾内容
  150. check_in.destroy
  151. end
  152. end
  153. def log_review_action(admin, action)
  154. # 记录管理员操作日志
  155. Rails.logger.info "ContentReport##{id} reviewed by #{admin.nickname} with action: #{action}"
  156. end
  157. end

app/models/daily_flower_stat.rb

0.0% lines covered

102 relevant lines. 0 lines covered and 102 lines missed.
    
  1. class DailyFlowerStat < ApplicationRecord
  2. # 关联
  3. belongs_to :reading_event
  4. # 验证
  5. validates :reading_event_id, :stats_date, :leaderboard_data, :generated_at, presence: true
  6. validates :stats_date, uniqueness: { scope: :reading_event_id }
  7. # 作用域
  8. scope :for_event, ->(event) { where(reading_event: event) }
  9. scope :for_date, ->(date) { where(stats_date: date) }
  10. scope :recent_first, -> { order(generated_at: :desc) }
  11. scope :generated_between, ->(start_date, end_date) { where(generated_at: start_date..end_date) }
  12. # 回调
  13. before_validation :set_generated_at, on: :create
  14. # 实例方法
  15. # 获取排行榜数据(解析JSON)
  16. def leaderboard
  17. return [] unless leaderboard_data.is_a?(Hash)
  18. leaderboard_data['rankings'] || []
  19. end
  20. # 获取前三名
  21. def top_three
  22. leaderboard.first(3)
  23. end
  24. # 获取指定用户的排名
  25. def user_ranking(user)
  26. return nil unless user
  27. leaderboard.find { |entry| entry['user_id'] == user.id }
  28. end
  29. # 获取分享文案
  30. def share_text_for_wechat
  31. return share_text if share_text.present?
  32. default_text = "🌸 #{reading_event.title} #{stats_date.strftime('%m月%d日')}小红花排行榜\n\n"
  33. default_text += "🏆 今日小红花TOP3:\n"
  34. top_three.each_with_index do |entry, index|
  35. user = User.find_by(id: entry['user_id'])
  36. next unless user
  37. emoji = ['🥇', '🥈', '🥉'][index]
  38. default_text += "#{emoji} #{user.nickname} - #{entry['total_flowers']}朵\n"
  39. end
  40. default_text += "\n💝 总计#{total_flowers_given}朵小红花,#{total_participants}位小伙伴参与"
  41. default_text
  42. end
  43. # 检查是否为今日统计
  44. def for_today?
  45. stats_date == Date.current
  46. end
  47. # 检查是否为昨日统计
  48. def for_yesterday?
  49. stats_date == Date.yesterday
  50. end
  51. # 增加分享次数
  52. def increment_share_count!
  53. increment!(:share_count)
  54. end
  55. # 生成分享图片URL(占位符,实际实现需要集成图片生成服务)
  56. def generate_share_image_url
  57. # 这里可以集成第三方图片生成服务,如:
  58. # - 使用Canvas API生成图片
  59. # - 使用微信小程序生成分享图片
  60. # - 使用第三方API服务
  61. timestamp = generated_at.to_i
  62. "https://api.example.com/share-images/daily-flower-stats/#{id}?t=#{timestamp}"
  63. end
  64. # API响应格式
  65. def as_json_for_api
  66. {
  67. id: id,
  68. reading_event: reading_event.as_json_for_api,
  69. stats_date: stats_date,
  70. leaderboard: leaderboard,
  71. top_three: top_three.map do |entry|
  72. user = User.find_by(id: entry['user_id'])
  73. {
  74. rank: entry['rank'],
  75. user: user&.as_json_for_api,
  76. total_flowers: entry['total_flowers'],
  77. flowers_received: entry['flowers_received'],
  78. flowers_given: entry['flowers_given']
  79. }
  80. end,
  81. statistics: {
  82. total_flowers_given: total_flowers_given,
  83. total_participants: total_participants,
  84. total_givers: total_givers,
  85. share_count: share_count
  86. },
  87. share_info: {
  88. image_url: share_image_url || generate_share_image_url,
  89. text: share_text_for_wechat,
  90. share_count: share_count
  91. },
  92. generated_at: generated_at,
  93. for_today: for_today?,
  94. for_yesterday: for_yesterday?
  95. }
  96. end
  97. # 类方法
  98. # 获取或创建指定日期的统计
  99. def self.get_or_create_daily_stat(event, date = Date.yesterday)
  100. find_or_create_by(reading_event: event, stats_date: date) do |stat|
  101. stat.generated_at = Time.current
  102. stat.generated_by = 'system_auto'
  103. end
  104. end
  105. # 检查是否已存在指定日期的统计
  106. def self.exists_for_date?(event, date)
  107. exists_by?(reading_event: event, stats_date: date)
  108. end
  109. # 获取活动的统计历史
  110. def self.event_statistics_history(event, limit: 30)
  111. for_event(event)
  112. .recent_first
  113. .limit(limit)
  114. end
  115. # 获取最近N天的统计
  116. def self.recent_statistics(days = 7)
  117. where(stats_date: (Date.current - days.days)..Date.current)
  118. .order(stats_date: :desc)
  119. end
  120. private
  121. def set_generated_at
  122. self.generated_at ||= Time.current
  123. self.generated_by ||= 'system_auto'
  124. end
  125. end

app/models/daily_leading.rb

0.0% lines covered

47 relevant lines. 0 lines covered and 47 lines missed.
    
  1. class DailyLeading < ApplicationRecord
  2. # 关联
  3. belongs_to :reading_schedule
  4. belongs_to :leader, class_name: "User"
  5. # 验证
  6. validates :reading_suggestion, presence: true
  7. validates :questions, presence: true
  8. validates :reading_schedule_id, uniqueness: { message: "今日已有领读内容" }
  9. # API序列化方法 - 标准化API响应格式
  10. def as_json_for_api(options = {})
  11. current_user = options[:current_user]
  12. result = {
  13. id: id,
  14. reading_suggestion: reading_suggestion,
  15. questions: questions,
  16. summary: summary,
  17. created_at: created_at,
  18. updated_at: updated_at,
  19. leader: leader.as_json_for_api
  20. }
  21. # 添加阅读计划信息
  22. if options[:include_schedule] && reading_schedule
  23. result[:reading_schedule] = {
  24. id: reading_schedule.id,
  25. day_number: reading_schedule.day_number,
  26. date: reading_schedule.date,
  27. reading_progress: reading_schedule.reading_progress
  28. }
  29. end
  30. # 添加活动信息
  31. if options[:include_event] && reading_schedule&.reading_event
  32. result[:reading_event] = {
  33. id: reading_schedule.reading_event.id,
  34. title: reading_schedule.reading_event.title
  35. }
  36. end
  37. # 添加当前用户的权限信息
  38. if current_user
  39. result[:interactions] = {
  40. can_edit: can_edit?(current_user),
  41. is_leader: leader_id == current_user.id
  42. }
  43. end
  44. result
  45. end
  46. private
  47. # 权限检查方法
  48. def can_edit?(current_user)
  49. return false unless current_user
  50. return true if current_user.any_admin? # 管理员可以编辑任何领读内容
  51. return true if leader_id == current_user.id # 领读人可以编辑自己的内容
  52. false
  53. end
  54. end

app/models/enrollment.rb

50.0% lines covered

26 relevant lines. 13 lines covered and 13 lines missed.
    
  1. 1 class Enrollment < ApplicationRecord
  2. # 关联
  3. 1 belongs_to :user
  4. 1 belongs_to :reading_event
  5. 1 has_many :check_ins, dependent: :destroy
  6. # 验证
  7. 1 validates :user_id, uniqueness: { scope: :reading_event_id, message: "已经报名该活动" }
  8. # 枚举
  9. 1 enum :payment_status, { unpaid: 0, paid: 1, refunded: 2 }
  10. 1 enum :role, { participant: 0, leader: 1 }
  11. # 计算打卡完成率
  12. 1 def completion_rate
  13. total_days = reading_event.reading_schedules.count
  14. return 0 if total_days.zero?
  15. completed_days = check_ins.where.not(status: :missed).count
  16. (completed_days.to_f / total_days * 100).round(2)
  17. end
  18. # 计算应退押金
  19. 1 def refund_amount_calculated
  20. reading_event.deposit * (completion_rate / 100.0)
  21. end
  22. # 权限检查方法
  23. 1 def is_current_leader?
  24. return false unless reading_event&.in_progress?
  25. reading_event.leader_id == user_id
  26. end
  27. 1 def is_current_daily_leader?(schedule)
  28. return false unless reading_event&.in_progress?
  29. return false unless schedule&.reading_event_id == reading_event_id
  30. # 检查是否是当天的领读人
  31. schedule.daily_leader_id == user_id &&
  32. schedule.date == Date.today
  33. end
  34. 1 def is_current_participant?
  35. reading_event&.in_progress? || reading_event&.enrolling?
  36. end
  37. # 活动结束时重置角色
  38. 1 def reset_roles_on_event_completion!
  39. return unless reading_event&.completed?
  40. update!(role: :participant) # 所有人都变回普通参与者
  41. end
  42. end

app/models/event_enrollment.rb

36.99% lines covered

146 relevant lines. 54 lines covered and 92 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: event_enrollments
  4. #
  5. # id :integer not null, primary key
  6. # reading_event_id :integer not null
  7. # user_id :integer not null
  8. # enrollment_type :string default("participant"), not null
  9. # status :string default("enrolled"), not null
  10. # enrollment_date :datetime not null
  11. # completion_rate :decimal(5, 2) default(0.0), not null
  12. # check_ins_count :integer default(0), not null
  13. # leader_days_count :integer default(0), not null
  14. # flowers_received_count :integer default(0), not null
  15. # fee_paid_amount :decimal(10, 2) default(0.0), not null
  16. # fee_refund_amount :decimal(10, 2) default(0.0), not null
  17. # refund_status :string default("pending"), not null
  18. # created_at :datetime not null
  19. # updated_at :datetime not null
  20. #
  21. # Indexes
  22. #
  23. # idx_event_enrollments_enrollment_date (enrollment_date)
  24. # idx_event_enrollments_enrollment_type (enrollment_type)
  25. # idx_event_enrollments_status (status)
  26. # index_event_enrollments_on_reading_event_id_and_user_id (reading_event_id, user_id) UNIQUE
  27. #
  28. # Foreign Keys
  29. #
  30. # fk_rails_... (reading_event_id => reading_events.id)
  31. # fk_rails_... (user_id => users.id)
  32. #
  33. 1 class EventEnrollment < ApplicationRecord
  34. # 参与类型枚举
  35. 1 enum :enrollment_type, {
  36. participant: 'participant', # 参与者
  37. observer: 'observer' # 围观者
  38. }, default: :participant
  39. # 报名状态枚举
  40. 1 enum :status, {
  41. enrolled: 'enrolled', # 已报名
  42. completed: 'completed', # 已完成
  43. cancelled: 'cancelled' # 已取消
  44. }, default: :enrolled
  45. # 退款状态枚举
  46. 1 enum :refund_status, {
  47. pending: 'pending', # 待处理
  48. refunded: 'refunded', # 已退款
  49. forfeited: 'forfeited' # 没收
  50. }, default: :pending
  51. # 关联关系
  52. 1 belongs_to :reading_event
  53. 1 belongs_to :user
  54. 1 has_many :check_ins, dependent: :destroy
  55. 1 has_many :received_flowers, class_name: 'Flower', foreign_key: :recipient_id, dependent: :destroy
  56. 1 has_many :given_flowers, class_name: 'Flower', foreign_key: :giver_id, dependent: :destroy
  57. 1 has_many :daily_leading_assignments, class_name: 'ReadingSchedule', foreign_key: :daily_leader_id, dependent: :nullify
  58. 1 has_many :participation_certificates, dependent: :destroy
  59. # 验证规则
  60. 1 validates :enrollment_date, presence: true
  61. 1 validates :completion_rate, numericality: {
  62. greater_than_or_equal_to: 0,
  63. less_than_or_equal_to: 100
  64. }
  65. 1 validates :fee_paid_amount, numericality: {
  66. greater_than_or_equal_to: 0
  67. }
  68. 1 validates :fee_refund_amount, numericality: {
  69. greater_than_or_equal_to: 0
  70. }
  71. 1 validate :cannot_enroll_if_event_completed
  72. 1 validate :unique_enrollment_per_event
  73. # 作用域
  74. 1 scope :participants, -> { where(enrollment_type: :participant) }
  75. 1 scope :observers, -> { where(enrollment_type: :observer) }
  76. 1 scope :enrolled, -> { where(status: :enrolled) }
  77. 1 scope :completed, -> { where(status: :completed) }
  78. 1 scope :cancelled, -> { where(status: :cancelled) }
  79. 1 scope :active, -> { where(status: [:enrolled, :completed]) }
  80. 1 scope :by_completion_rate, ->(direction = :desc) { order(completion_rate: direction) }
  81. 1 scope :by_flowers_count, ->(direction = :desc) { order(flowers_received_count: direction) }
  82. # 类方法:计算报名统计
  83. 1 def self.calculate_enrollment_statistics
  84. all_enrollments = includes(:user, :reading_event)
  85. {
  86. total_enrollments: all_enrollments.count,
  87. active_enrollments: all_enrollments.where(status: 'enrolled').count,
  88. completed_enrollments: all_enrollments.where(status: 'completed').count,
  89. cancelled_enrollments: all_enrollments.where(status: 'cancelled').count,
  90. participants_count: all_enrollments.where(enrollment_type: 'participant').count,
  91. observers_count: all_enrollments.where(enrollment_type: 'observer').count,
  92. total_fees_collected: all_enrollments.sum(:fee_paid_amount),
  93. total_refunds_processed: all_enrollments.sum(:fee_refund_amount),
  94. enrollment_trend: calculate_enrollment_trend(all_enrollments),
  95. completion_trend: calculate_completion_trend(all_enrollments)
  96. }
  97. end
  98. # 委托方法
  99. 1 delegate :title, :book_name, :activity_mode, :completion_standard, to: :reading_event, prefix: true
  100. 1 delegate :nickname, to: :user, prefix: true
  101. # 状态方法(公开方法供其他模型调用)
  102. 1 def can_participate?
  103. enrolled? && participant?
  104. end
  105. 1 def can_check_in?
  106. can_participate? && reading_event.in_progress?
  107. end
  108. 1 def can_receive_flowers?
  109. can_participate? && check_ins.any?
  110. end
  111. 1 def can_give_flowers?
  112. can_participate? && reading_event.in_progress?
  113. end
  114. 1 def can_cancel?
  115. enrolled? && !reading_event.in_progress?
  116. end
  117. 1 def cancellation_error_message
  118. return "报名已取消,无法再次取消" if cancelled?
  119. return "活动已开始,无法取消报名" if reading_event.in_progress?
  120. return "活动已完成,无法取消报名" if reading_event.completed?
  121. "无法取消报名"
  122. end
  123. 1 def is_completed?
  124. completion_rate >= reading_event.completion_standard
  125. end
  126. # 统计方法
  127. 1 def update_completion_rate!
  128. new_rate = calculate_completion_rate
  129. update!(completion_rate: new_rate)
  130. # 如果完成率达到标准,更新状态
  131. if is_completed? && enrolled?
  132. update!(status: :completed)
  133. end
  134. end
  135. 1 def calculate_completion_rate
  136. case reading_event.activity_mode
  137. when 'note_checkin'
  138. calculate_note_checkin_completion
  139. when 'free_discussion'
  140. calculate_free_discussion_completion
  141. when 'video_conference'
  142. calculate_video_conference_completion
  143. when 'offline_meeting'
  144. calculate_offline_meeting_completion
  145. else
  146. 0.0
  147. end
  148. end
  149. # 费用相关方法
  150. 1 def calculate_refund_amount
  151. return 0.0 if reading_event.fee_type != 'deposit'
  152. DepositRefundCalculator.calculate_refund_amount(user, reading_event)
  153. end
  154. 1 def process_refund!
  155. return unless reading_event.fee_type == 'deposit'
  156. return if refund_status != 'pending'
  157. refund_amount = calculate_refund_amount
  158. transaction do
  159. update!(
  160. fee_refund_amount: refund_amount,
  161. refund_status: refund_amount > 0 ? 'refunded' : 'forfeited'
  162. )
  163. # 这里应该调用实际的退款服务
  164. # RefundService.process(user, refund_amount) if refund_amount > 0
  165. end
  166. end
  167. # 证书相关方法
  168. 1 def eligible_for_completion_certificate?
  169. is_completed? && participation_certificates.where(certificate_type: 'completion').empty?
  170. end
  171. 1 def eligible_for_flower_certificate?(rank = nil)
  172. return false unless flowers_received_count > 0
  173. if rank
  174. # 检查是否在指定排名
  175. top_rankings = reading_event.event_enrollments
  176. .where('flowers_received_count > 0')
  177. .order(flowers_received_count: :desc)
  178. .limit(rank)
  179. top_rankings.include?(self) &&
  180. participation_certificates.where(certificate_type: "flower_top#{rank}").empty?
  181. else
  182. # 只要有小红花就可能有资格
  183. flowers_received_count > 0
  184. end
  185. end
  186. # 通知方法
  187. 1 def notify_enrollment_confirmation
  188. # 发送报名确认通知
  189. EnrollmentNotificationService.confirm_enrollment(self)
  190. end
  191. 1 def notify_completion_achievement
  192. return unless is_completed?
  193. # 发送完成成就通知
  194. EnrollmentNotificationService.notify_completion(self)
  195. end
  196. 1 def notify_certificate_issued(certificate)
  197. # 发送证书颁发通知
  198. EnrollmentNotificationService.notify_certificate_issued(self, certificate)
  199. end
  200. # API响应格式化
  201. 1 def as_json_for_api(options = {})
  202. base_data = {
  203. id: id,
  204. enrollment_type: enrollment_type,
  205. status: status,
  206. enrollment_date: enrollment_date,
  207. completion_rate: completion_rate,
  208. check_ins_count: check_ins_count,
  209. leader_days_count: leader_days_count,
  210. flowers_received_count: flowers_received_count,
  211. fee_paid_amount: fee_paid_amount,
  212. fee_refund_amount: fee_refund_amount,
  213. refund_status: refund_status,
  214. created_at: created_at,
  215. updated_at: updated_at,
  216. is_completed: is_completed?,
  217. can_participate: can_participate?,
  218. can_check_in: can_check_in?,
  219. can_receive_flowers: can_receive_flowers?,
  220. can_give_flowers: can_give_flowers?,
  221. can_cancel: can_cancel?
  222. }
  223. # 可选包含关联数据
  224. if options[:include_user]
  225. base_data[:user] = user.as_json_for_api
  226. end
  227. if options[:include_reading_event]
  228. base_data[:reading_event] = reading_event.as_json_for_api
  229. end
  230. if options[:include_check_ins]
  231. base_data[:check_ins] = check_ins.includes(:user).map do |check_in|
  232. check_in.as_json_for_api(include_user: false)
  233. end
  234. end
  235. if options[:include_flowers]
  236. base_data[:flowers] = received_flowers.includes(:giver).map do |flower|
  237. {
  238. id: flower.id,
  239. amount: flower.amount,
  240. flower_type: flower.flower_type,
  241. comment: flower.comment,
  242. giver: flower.giver.as_json_for_api,
  243. created_at: flower.created_at
  244. }
  245. end
  246. end
  247. if options[:include_certificates]
  248. base_data[:certificates] = participation_certificates.map do |cert|
  249. {
  250. id: cert.id,
  251. certificate_type: cert.certificate_type,
  252. certificate_number: cert.certificate_number,
  253. issued_at: cert.issued_at,
  254. is_public: cert.is_public,
  255. certificate_url: cert.certificate_url
  256. }
  257. end
  258. end
  259. if options[:include_statistics]
  260. base_data[:statistics] = {
  261. completion_percentage: completion_rate,
  262. attendance_rate: reading_event.reading_schedules.any? ? (check_ins_count.to_f / reading_event.reading_schedules.count * 100).round(2) : 0,
  263. flower_ranking_in_event: calculate_flower_ranking_in_event
  264. }
  265. end
  266. base_data
  267. end
  268. 1 private
  269. # 验证方法
  270. 1 def cannot_enroll_if_event_completed
  271. if reading_event.completed? && enrolled?
  272. errors.add(:base, "不能报名已完成的活动")
  273. end
  274. end
  275. 1 def unique_enrollment_per_event
  276. return unless reading_event_id && user_id
  277. existing = EventEnrollment.where(
  278. reading_event_id: reading_event_id,
  279. user_id: user_id
  280. ).where.not(id: id)
  281. if existing.exists?
  282. errors.add(:base, "已经报名过此活动")
  283. end
  284. end
  285. # 完成率计算方法
  286. 1 def calculate_note_checkin_completion
  287. schedules = reading_event.reading_schedules
  288. total_days = calculate_total_reading_days(schedules, reading_event)
  289. return 0.0 if total_days == 0
  290. # 获取实际打卡次数
  291. check_ins_count = check_ins
  292. .where(schedule: schedules)
  293. .where.not(status: 'supplement')
  294. .count
  295. # 获取担任领读天数
  296. leader_days_count = daily_leading_assignments
  297. .where(reading_schedule: schedules)
  298. .count
  299. # 计算完成率:(打卡次数 + 担任领读天数) / 总天数
  300. completed_days = check_ins_count + leader_days_count
  301. (completed_days.to_f / total_days * 100).round(2)
  302. end
  303. 1 def calculate_free_discussion_completion
  304. # 自由讨论模式:基于参与度计算
  305. # 这里可以基于发帖、回复等互动数据计算
  306. # 暂时使用打卡次数作为基础指标
  307. schedules = reading_event.reading_schedules
  308. total_days = calculate_total_reading_days(schedules, reading_event)
  309. return 0.0 if total_days == 0
  310. participation_count = check_ins.where(schedule: schedules).count
  311. (participation_count.to_f / total_days * 100).round(2)
  312. end
  313. 1 def calculate_video_conference_completion
  314. # 视频会议模式:基于出席率计算
  315. # 这里需要检查用户的会议出席记录
  316. # 暂时返回基于日程的简单计算
  317. schedules = reading_event.reading_schedules
  318. total_sessions = schedules.count
  319. return 0.0 if total_sessions == 0
  320. # 假设用户参与了所有会议(实际应该检查出席记录)
  321. attendance_count = total_sessions # 这里应该是实际的出席次数
  322. (attendance_count.to_f / total_sessions * 100).round(2)
  323. end
  324. 1 def calculate_offline_meeting_completion
  325. # 线下交流模式:基于出席率计算
  326. # 类似视频会议模式,但针对线下活动
  327. schedules = reading_event.reading_schedules
  328. total_meetings = schedules.count
  329. return 0.0 if total_meetings == 0
  330. # 假设用户参与了所有会议(实际应该检查出席记录)
  331. attendance_count = total_meetings # 这里应该是实际的出席次数
  332. (attendance_count.to_f / total_meetings * 100).round(2)
  333. end
  334. 1 def calculate_total_reading_days(schedules, event)
  335. if event.weekend_rest
  336. # 排除周末
  337. schedules.where.not(date: [Date::SATURDAY, Date::SUNDAY]).count
  338. else
  339. schedules.count
  340. end
  341. end
  342. 1 def calculate_flower_ranking_in_event
  343. return nil unless flowers_received_count > 0
  344. # 获取活动中所有有小红花的参与者,按数量排序
  345. ranked_participants = reading_event.event_enrollments
  346. .where('flowers_received_count > 0')
  347. .order(flowers_received_count: :desc)
  348. .pluck(:id)
  349. ranked_participants.index(id) + 1
  350. end
  351. end

app/models/flower.rb

46.34% lines covered

41 relevant lines. 19 lines covered and 22 lines missed.
    
  1. 1 class Flower < ApplicationRecord
  2. # 关联
  3. 1 belongs_to :check_in
  4. 1 belongs_to :giver, class_name: "User"
  5. 1 belongs_to :recipient, class_name: "User"
  6. 1 belongs_to :reading_schedule
  7. 1 has_many :comments, as: :commentable, dependent: :destroy
  8. # 验证
  9. 1 validates :check_in_id, uniqueness: { message: "该打卡已获得小红花" }
  10. 1 validate :daily_flower_limit
  11. 1 validate :giver_is_daily_leader
  12. # 获取赠送者显示名称
  13. 1 def giver_display_name
  14. return '匿名用户' if is_anonymous?
  15. giver&.nickname || '未知用户'
  16. end
  17. # 获取接收者显示名称
  18. 1 def recipient_display_name
  19. recipient&.nickname || '未知用户'
  20. end
  21. # 评论相关方法
  22. 1 def add_comment(user, content)
  23. comments.create!(
  24. user: user,
  25. content: content,
  26. commentable: self
  27. )
  28. end
  29. 1 def can_receive_comment?(user)
  30. # 小红花接收者可以查看和回复评论
  31. return true if user.present?
  32. # 这里可以添加更多权限逻辑
  33. true
  34. end
  35. 1 def recent_comments(limit = 5)
  36. comments.includes(:user).order(created_at: :desc).limit(limit)
  37. end
  38. 1 def comments_count
  39. comments.count
  40. end
  41. # API响应格式
  42. 1 def as_json_for_api(options = {})
  43. base_data = {
  44. id: id,
  45. giver: is_anonymous? ? { id: nil, nickname: '匿名用户' } : giver.as_json_for_api,
  46. recipient: recipient.as_json_for_api,
  47. check_in: {
  48. id: check_in.id,
  49. content: check_in.content.truncate(100),
  50. user: check_in.user.as_json_for_api,
  51. created_at: check_in.created_at
  52. },
  53. amount: amount,
  54. flower_type: flower_type,
  55. comment: comment,
  56. is_anonymous: is_anonymous,
  57. created_at: created_at,
  58. giver_display_name: giver_display_name,
  59. recipient_display_name: recipient_display_name,
  60. comments_count: comments_count
  61. }
  62. # 可选包含评论数据
  63. if options[:include_comments]
  64. base_data[:comments] = recent_comments.map(&:as_json_for_api)
  65. end
  66. if options[:include_comment_stats]
  67. base_data[:comment_stats] = {
  68. total_count: comments_count,
  69. recent_count: recent_comments.count,
  70. latest_comment: recent_comments.first&.as_json_for_api
  71. }
  72. end
  73. base_data
  74. end
  75. 1 private
  76. # 每日最多发放3朵小红花
  77. 1 def daily_flower_limit
  78. daily_count = Flower.where(
  79. giver: giver,
  80. reading_schedule: reading_schedule
  81. ).count
  82. if daily_count >= 3 && !persisted?
  83. errors.add(:base, "每日最多发放3朵小红花")
  84. end
  85. end
  86. # 只有领读人可以发放小红花(考虑3天权限窗口)
  87. 1 def giver_is_daily_leader
  88. return if reading_schedule.blank? || giver.blank?
  89. # 检查是否有权限发放小红花(当天和后一天权限)
  90. event = reading_schedule.reading_event
  91. unless event&.can_give_flowers?(giver, reading_schedule)
  92. errors.add(:base, "只有领读人可以在当天或后一天发放小红花")
  93. end
  94. end
  95. end

app/models/flower_certificate.rb

42.0% lines covered

50 relevant lines. 21 lines covered and 29 lines missed.
    
  1. 1 class FlowerCertificate < ApplicationRecord
  2. # 关联
  3. 1 belongs_to :user
  4. 1 belongs_to :reading_event
  5. # 验证
  6. 1 validates :rank, inclusion: { in: [1, 2, 3] }
  7. 1 validates :total_flowers, numericality: { greater_than: 0 }
  8. 1 validates :certificate_id, presence: true, uniqueness: true
  9. # 作用域
  10. 1 scope :for_user, ->(user) { where(user: user) }
  11. 1 scope :for_event, ->(event) { where(reading_event: event) }
  12. 1 scope :ranked, -> { order(:rank) }
  13. # 回调
  14. 1 before_validation :generate_certificate_id, on: :create
  15. # 实例方法
  16. # 获取排名显示
  17. 1 def rank_display
  18. case rank
  19. when 1 then '🥇 第一名'
  20. when 2 then '🥈 第二名'
  21. when 3 then '🥉 第三名'
  22. else "第#{rank}名"
  23. end
  24. end
  25. # 获取荣誉等级
  26. 1 def honor_level
  27. case rank
  28. when 1 then '优秀小红花达人'
  29. when 2 then '小红花之星'
  30. when 3 then '小红花爱好者'
  31. else '小红花参与者'
  32. end
  33. end
  34. # 检查是否是前三名
  35. 1 def is_top_three?
  36. rank <= 3
  37. end
  38. # 生成证书图片路径
  39. 1 def certificate_image_path
  40. "/certificates/flower_certificate_#{certificate_id}.png"
  41. end
  42. # 生成证书分享链接
  43. 1 def share_url
  44. "#{Rails.application.config.base_url}/flower_certificates/#{certificate_id}"
  45. end
  46. # 类方法
  47. # 为活动生成前三名证书
  48. 1 def self.generate_top_three_certificates(event)
  49. # 计算活动中的小红花排行榜
  50. flower_stats = Flower.joins(:recipient)
  51. .joins(check_in: :event_enrollment)
  52. .where(event_enrollments: { reading_event_id: event.id })
  53. .group('recipients.id')
  54. .sum(:amount)
  55. # 排序并获取前三名
  56. top_users = flower_stats.sort_by { |user_id, flowers| -flowers }
  57. .first(3)
  58. .map.with_index(1) { |(user_id, flowers), index| [user_id, flowers, index] }
  59. certificates = []
  60. top_users.each do |user_id, total_flowers, rank|
  61. user = User.find(user_id)
  62. certificate = create!(
  63. user: user,
  64. reading_event: event,
  65. rank: rank,
  66. total_flowers: total_flowers
  67. )
  68. certificates << certificate
  69. end
  70. certificates
  71. end
  72. # 获取用户的所有小红花证书
  73. 1 def self.for_user_all(user)
  74. for_user(user).ranked
  75. end
  76. # 检查证书是否有效
  77. 1 def valid_certificate?
  78. reading_event&.status == 'completed'
  79. end
  80. # API响应格式
  81. 1 def as_json_for_api
  82. {
  83. id: id,
  84. certificate_id: certificate_id,
  85. rank: rank,
  86. rank_display: rank_display,
  87. honor_level: honor_level,
  88. total_flowers: total_flowers,
  89. user: user.as_json_for_api,
  90. reading_event: reading_event.as_json_for_api,
  91. is_top_three: is_top_three?,
  92. valid_certificate: valid_certificate?,
  93. share_url: share_url,
  94. certificate_image_url: certificate_image_path,
  95. created_at: created_at
  96. }
  97. end
  98. 1 private
  99. # 生成唯一的证书编号
  100. 1 def generate_certificate_id
  101. return if certificate_id.present?
  102. loop do
  103. id = "FC#{Time.current.strftime('%Y%m%d')}#{SecureRandom.hex(4).upcase}"
  104. break self.certificate_id = id unless FlowerCertificate.exists?(certificate_id: id)
  105. end
  106. end
  107. end

app/models/flower_quota.rb

48.08% lines covered

52 relevant lines. 25 lines covered and 27 lines missed.
    
  1. 1 class FlowerQuota < ApplicationRecord
  2. 1 self.table_name = 'flower_quotas'
  3. # 关联
  4. 1 belongs_to :user
  5. 1 belongs_to :reading_event
  6. # 验证
  7. 1 validates :max_flowers, numericality: { greater_than: 0 }
  8. 1 validates :used_flowers, numericality: { greater_than_or_equal_to: 0 }
  9. 1 validates :quota_date, presence: true
  10. # 作用域
  11. 1 scope :for_user, ->(user) { where(user: user) }
  12. 1 scope :for_event, ->(event) { where(reading_event: event) }
  13. 1 scope :for_date, ->(date) { where(quota_date: date) }
  14. 1 scope :current, -> { where(quota_date: Date.current) }
  15. 1 scope :recent, -> { order(quota_date: :desc) }
  16. # 实例方法
  17. # 检查是否还有赠送额度
  18. 1 def can_give_flower?(amount = 1)
  19. (used_flowers + amount) <= max_flowers
  20. end
  21. # 获取剩余可赠送数量
  22. 1 def remaining_flowers
  23. max_flowers - used_flowers
  24. end
  25. # 使用小红花(每日配额版本)
  26. 1 def use_flowers!(amount = 1)
  27. return false unless can_give_flower?(amount)
  28. transaction do
  29. increment!(:used_flowers, amount)
  30. increment!(:give_count_today, amount)
  31. touch(:last_given_at)
  32. end
  33. true
  34. end
  35. # 重置使用数量(每日重置)
  36. 1 def reset_daily_usage!
  37. update!(used_flowers: 0, give_count_today: 0)
  38. end
  39. # 获取使用率
  40. 1 def usage_percentage
  41. return 0 if max_flowers == 0
  42. (used_flowers.to_f / max_flowers * 100).round(2)
  43. end
  44. # 检查是否为今日配额
  45. 1 def for_today?
  46. quota_date == Date.current
  47. end
  48. # 检查是否为活动日
  49. 1 def activity_day?(event)
  50. event.start_date <= quota_date && quota_date <= event.end_date && !event.weekend_rest?
  51. end
  52. # 类方法
  53. # 获取或创建每日配额
  54. 1 def self.get_or_create_daily_quota(user, event, date = Date.current, max_flowers: 3)
  55. find_or_create_by(user: user, reading_event: event, quota_date: date) do |quota|
  56. quota.max_flowers = max_flowers
  57. quota.used_flowers = 0
  58. quota.give_count_today = 0
  59. end
  60. end
  61. # 兼容性方法 - 为用户和活动创建或获取配额
  62. 1 def self.get_or_create_quota(user, event, max_flowers: 3)
  63. get_or_create_daily_quota(user, event, Date.current, max_flowers)
  64. end
  65. # 检查用户每日配额
  66. 1 def self.check_daily_quota(user, event, date = Date.current, amount = 1)
  67. quota = find_by(user: user, reading_event: event, quota_date: date)
  68. return { can_give: false, remaining: 0, is_activity_day: false } unless quota
  69. {
  70. can_give: quota.can_give_flower?(amount),
  71. remaining: quota.remaining_flowers,
  72. used: quota.used_flowers,
  73. max: quota.max_flowers,
  74. is_activity_day: quota.activity_day?(event),
  75. quota_date: quota.quota_date
  76. }
  77. end
  78. # 检查用户在活动中的配额(兼容性方法)
  79. 1 def self.check_quota(user, event, amount = 1)
  80. result = check_daily_quota(user, event, Date.current, amount)
  81. {
  82. can_give: result[:can_give],
  83. remaining: result[:remaining],
  84. used: result[:used],
  85. max: result[:max]
  86. }
  87. end
  88. # 获取用户在活动中的历史配额
  89. 1 def self.user_quota_history(user, event, limit: 30)
  90. for_user(user).for_event(event).recent.limit(limit)
  91. end
  92. # 获取活动在某日的总配额统计
  93. 1 def self.daily_quota_stats(event, date = Date.current)
  94. quotas = for_event(event).for_date(date)
  95. {
  96. date: date,
  97. total_users: quotas.count,
  98. total_flowers_available: quotas.sum(:max_flowers),
  99. total_flowers_used: quotas.sum(:used_flowers),
  100. usage_rate: quotas.count > 0 ? (quotas.sum(:used_flowers).to_f / quotas.sum(:max_flowers) * 100).round(2) : 0
  101. }
  102. end
  103. end

app/models/like.rb

71.11% lines covered

45 relevant lines. 32 lines covered and 13 lines missed.
    
  1. 1 class Like < ApplicationRecord
  2. 1 belongs_to :user, counter_cache: :likes_given_count
  3. 1 belongs_to :target, polymorphic: true
  4. # 验证
  5. 1 validates :user_id, uniqueness: { scope: [:target_type, :target_id] }
  6. # 回调:维护目标对象的counter_cache
  7. 1 after_create :increment_target_counter
  8. 1 after_destroy :decrement_target_counter
  9. # 类方法:创建点赞
  10. 1 def self.like!(user, target)
  11. 25 return false unless user && target
  12. 23 like = find_or_initialize_by(
  13. user: user,
  14. target: target
  15. )
  16. 23 if like.new_record?
  17. 22 like.save!
  18. 22 true
  19. else
  20. 1 false # 已经点赞
  21. end
  22. end
  23. # 类方法:取消点赞
  24. 1 def self.unlike!(user, target)
  25. 6 return false unless user && target
  26. 4 like = find_by(
  27. user: user,
  28. target: target
  29. )
  30. 4 if like
  31. 3 like.destroy!
  32. 3 true
  33. else
  34. 1 false # 未点赞
  35. end
  36. end
  37. # 类方法:检查是否点赞
  38. 1 def self.liked?(user, target)
  39. 11 return false unless user && target
  40. 9 exists?(user: user, target: target)
  41. end
  42. # API序列化方法 - 标准化API响应格式
  43. 1 def as_json_for_api(options = {})
  44. result = {
  45. id: id,
  46. user: user.as_json_for_api,
  47. target_type: target_type,
  48. target_id: target_id,
  49. created_at: created_at
  50. }
  51. # 添加目标对象信息
  52. if options[:include_target] && target
  53. result[:target] = if target.respond_to?(:as_json_for_api)
  54. target.as_json_for_api(options)
  55. else
  56. {
  57. type: target_type,
  58. id: target.id,
  59. title: target_title
  60. }
  61. end
  62. end
  63. result
  64. end
  65. 1 private
  66. # 获取目标对象的标题
  67. 1 def target_title
  68. return unless target
  69. case target_type
  70. when 'Post'
  71. target.title
  72. when 'Comment'
  73. target.content.truncate(50)
  74. when 'CheckIn'
  75. "第#{target.day_number}天打卡"
  76. when 'ReadingEvent'
  77. target.title
  78. else
  79. target_type
  80. end
  81. end
  82. # 增加目标对象的计数器
  83. 1 def increment_target_counter
  84. 35 case target_type
  85. when 'Post'
  86. 35 target.increment_likes_count if target.respond_to?(:increment_likes_count)
  87. end
  88. end
  89. # 减少目标对象的计数器
  90. 1 def decrement_target_counter
  91. 5 case target_type
  92. when 'Post'
  93. 5 target.decrement_likes_count if target.respond_to?(:decrement_likes_count)
  94. end
  95. end
  96. end

app/models/notification.rb

41.56% lines covered

77 relevant lines. 32 lines covered and 45 lines missed.
    
  1. # 通知模型
  2. # 用于管理系统中各种用户通知,包括小红花相关通知、评论通知、活动通知等
  3. 1 class Notification < ApplicationRecord
  4. # 关联关系
  5. 1 belongs_to :recipient, class_name: 'User'
  6. 1 belongs_to :actor, class_name: 'User'
  7. 1 belongs_to :notifiable, polymorphic: true
  8. # 验证规则
  9. 1 validates :recipient, presence: true
  10. 1 validates :actor, presence: true
  11. 1 validates :notification_type, presence: true, inclusion: { in: %w[flower_received flower_comment activity_update event_approved event_rejected] }
  12. 1 validates :title, presence: true, length: { maximum: 100 }
  13. 1 validates :content, presence: true, length: { maximum: 500 }
  14. # 作用域
  15. 1 scope :unread, -> { where(read: false) }
  16. 1 scope :read, -> { where(read: true) }
  17. 1 scope :recent, -> { order(created_at: :desc) }
  18. 1 scope :by_type, ->(type) { where(notification_type: type) }
  19. 1 scope :for_recipient, ->(user) { where(recipient: user) }
  20. # 通知类型常量
  21. NOTIFICATION_TYPES = {
  22. 1 flower_received: 'flower_received', # 收到小红花
  23. flower_comment: 'flower_comment', # 小红花被评论
  24. activity_update: 'activity_update', # 活动更新
  25. event_approved: 'event_approved', # 活动审批通过
  26. event_rejected: 'event_rejected' # 活动审批拒绝
  27. }.freeze
  28. # 默认排序
  29. 3 default_scope -> { order(created_at: :desc) }
  30. # 实例方法
  31. # 标记为已读
  32. 1 def mark_as_read!
  33. update!(read: true, read_at: Time.current) unless read?
  34. end
  35. # 标记为未读
  36. 1 def mark_as_unread!
  37. update!(read: false, read_at: nil)
  38. end
  39. # 是否已读
  40. 1 def read?
  41. read
  42. end
  43. # 是否未读
  44. 1 def unread?
  45. !read
  46. end
  47. # 获取通知的URL链接
  48. 1 def action_url
  49. case notification_type
  50. when 'flower_received', 'flower_comment'
  51. if notifiable_type == 'Flower'
  52. "/flowers/#{notifiable_id}"
  53. elsif notifiable_type == 'Comment'
  54. comment = Comment.find_by(id: notifiable_id)
  55. if comment&.commentable_type == 'Flower'
  56. "/flowers/#{comment.commentable_id}#comment-#{comment.id}"
  57. end
  58. end
  59. when 'activity_update', 'event_approved', 'event_rejected'
  60. if notifiable_type == 'ReadingEvent'
  61. "/events/#{notifiable_id}"
  62. end
  63. else
  64. '#'
  65. end
  66. end
  67. # 获取通知图标类型
  68. 1 def icon_type
  69. case notification_type
  70. when 'flower_received'
  71. 'flower'
  72. when 'flower_comment'
  73. 'comment'
  74. when 'activity_update'
  75. 'activity'
  76. when 'event_approved'
  77. 'approved'
  78. when 'event_rejected'
  79. 'rejected'
  80. else
  81. 'notification'
  82. end
  83. end
  84. # 格式化创建时间
  85. 1 def formatted_created_at
  86. case Time.current - created_at
  87. when 0..59.seconds
  88. '刚刚'
  89. when 1..59.minutes
  90. "#{(Time.current - created_at).to_i / 60}分钟前"
  91. when 1..23.hours
  92. "#{(Time.current - created_at).to_i / 3600}小时前"
  93. when 1..29.days
  94. "#{(Time.current - created_at).to_i / 86400}天前"
  95. else
  96. created_at.strftime('%m-%d %H:%M')
  97. end
  98. end
  99. # API响应格式
  100. 1 def as_json_for_api(options = {})
  101. base_data = {
  102. id: id,
  103. notification_type: notification_type,
  104. title: title,
  105. content: content,
  106. read: read,
  107. read_at: read_at,
  108. created_at: created_at,
  109. formatted_created_at: formatted_created_at,
  110. action_url: action_url,
  111. icon_type: icon_type
  112. }
  113. # 包含关联数据
  114. if options[:include_actor]
  115. base_data[:actor] = actor.as_json_for_api
  116. end
  117. if options[:include_notifiable]
  118. base_data[:notifiable] = if notifiable
  119. {
  120. type: notifiable_type,
  121. id: notifiable_id,
  122. data: notifiable.as_json_for_api
  123. }
  124. else
  125. nil
  126. end
  127. end
  128. base_data
  129. end
  130. # 类方法
  131. # 创建小红花通知
  132. 1 def self.create_flower_notification(recipient, actor, flower)
  133. create!(
  134. recipient: recipient,
  135. actor: actor,
  136. notifiable: flower,
  137. notification_type: NOTIFICATION_TYPES[:flower_received],
  138. title: '收到小红花',
  139. content: "#{actor.nickname} 给了你一朵小红花:#{flower.comment.presence || '很棒的表现!'}"
  140. )
  141. end
  142. # 创建评论通知
  143. 1 def self.create_comment_notification(recipient, actor, comment)
  144. create!(
  145. recipient: recipient,
  146. actor: actor,
  147. notifiable: comment,
  148. notification_type: NOTIFICATION_TYPES[:flower_comment],
  149. title: '新的评论',
  150. content: "#{actor.nickname} 评论了你的小红花:#{comment.content.truncate(50)}"
  151. )
  152. end
  153. # 创建活动更新通知
  154. 1 def self.create_activity_notification(recipient, actor, event, update_type, message)
  155. create!(
  156. recipient: recipient,
  157. actor: actor,
  158. notifiable: event,
  159. notification_type: NOTIFICATION_TYPES[:activity_update],
  160. title: '活动更新',
  161. content: message
  162. )
  163. end
  164. # 创建活动审批通知
  165. 1 def self.create_event_approval_notification(recipient, actor, event, approved)
  166. notification_type = approved ? NOTIFICATION_TYPES[:event_approved] : NOTIFICATION_TYPES[:event_rejected]
  167. title = approved ? '活动审批通过' : '活动审批拒绝'
  168. create!(
  169. recipient: recipient,
  170. actor: actor,
  171. notifiable: event,
  172. notification_type: notification_type,
  173. title: title,
  174. content: "#{actor.nickname} #{approved ? '通过了' : '拒绝了'}你的活动申请:#{event.title}"
  175. )
  176. end
  177. # 批量标记为已读
  178. 1 def self.mark_all_as_read_for(recipient)
  179. where(recipient: recipient, read: false).update_all(read: true, read_at: Time.current)
  180. end
  181. # 获取用户未读通知数量
  182. 1 def self.unread_count_for(recipient)
  183. where(recipient: recipient, read: false).count
  184. end
  185. # 获取用户最近的通知
  186. 1 def self.recent_for(recipient, limit = 10)
  187. where(recipient: recipient).limit(limit)
  188. end
  189. # 清理过期通知(保留30天)
  190. 1 def self.cleanup_old_notifications(days = 30)
  191. where('created_at < ?', days.days.ago).delete_all
  192. end
  193. end

app/models/participation_certificate.rb

0.0% lines covered

211 relevant lines. 0 lines covered and 211 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: participation_certificates
  4. #
  5. # id :integer not null, primary key
  6. # reading_event_id :integer not null
  7. # user_id :integer not null
  8. # certificate_type :string not null
  9. # certificate_number: string not null
  10. # issued_at :datetime not null
  11. # achievement_data :text
  12. # certificate_url :string
  13. # is_public :boolean default(TRUE), not null
  14. # created_at :datetime not null
  15. # updated_at :datetime not null
  16. #
  17. # Indexes
  18. #
  19. # idx_certificates_event_id (reading_event_id)
  20. # idx_certificates_is_public (is_public)
  21. # idx_certificates_issued_at (issued_at)
  22. # idx_certificates_type (certificate_type)
  23. # idx_certificates_user_id (user_id)
  24. # index_participation_certificates_on_certificate_number (certificate_number) UNIQUE
  25. #
  26. # Foreign Keys
  27. #
  28. # fk_rails_... (reading_event_id => reading_events.id)
  29. # fk_rails_... (user_id => users.id)
  30. #
  31. class ParticipationCertificate < ApplicationRecord
  32. # 证书类型枚举
  33. enum :certificate_type, {
  34. completion: 'completion', # 完成证书
  35. flower_top1: 'flower_top1', # 小红花第一名证书
  36. flower_top2: 'flower_top2', # 小红花第二名证书
  37. flower_top3: 'flower_top3', # 小红花第三名证书
  38. custom: 'custom' # 自定义证书
  39. }, default: :completion
  40. # 关联关系
  41. belongs_to :reading_event
  42. belongs_to :user
  43. # 验证规则
  44. validates :certificate_number, presence: true, uniqueness: true
  45. validates :issued_at, presence: true
  46. validates :achievement_data, presence: true
  47. validate :certificate_number_format
  48. validate :user_must_be_event_participant
  49. validate :certificate_requirements_met
  50. # 作用域
  51. scope :is_public, -> { where(is_public: true) }
  52. scope :is_private, -> { where(is_public: false) }
  53. scope :completion, -> { where(certificate_type: :completion) }
  54. scope :flower_top, -> { where(certificate_type: [:flower_top1, :flower_top2, :flower_top3]) }
  55. scope :custom, -> { where(certificate_type: :custom) }
  56. scope :recent, -> { order(issued_at: :desc) }
  57. # 委托方法
  58. delegate :title, :book_name, to: :reading_event, prefix: true
  59. delegate :nickname, to: :user, prefix: true
  60. # 状态方法
  61. def completion_certificate?
  62. certificate_type == 'completion'
  63. end
  64. def flower_certificate?
  65. ['flower_top1', 'flower_top2', 'flower_top3'].include?(certificate_type)
  66. end
  67. def custom_certificate?
  68. certificate_type == 'custom'
  69. end
  70. def flower_rank
  71. return nil unless flower_certificate?
  72. case certificate_type
  73. when 'flower_top1' then 1
  74. when 'flower_top2' then 2
  75. when 'flower_top3' then 3
  76. end
  77. end
  78. # 证书生成方法
  79. def self.generate_completion_certificate(enrollment)
  80. return nil unless enrollment.is_completed?
  81. return nil if exists?(user: enrollment.user, reading_event: enrollment.reading_event, certificate_type: :completion)
  82. certificate_number = generate_certificate_number('COMP')
  83. achievement_data = build_completion_achievement_data(enrollment)
  84. create!(
  85. user: enrollment.user,
  86. reading_event: enrollment.reading_event,
  87. certificate_type: :completion,
  88. certificate_number: certificate_number,
  89. issued_at: Time.current,
  90. achievement_data: achievement_data.to_json,
  91. is_public: true
  92. )
  93. end
  94. def self.generate_flower_certificate(enrollment, rank)
  95. return nil unless enrollment.flowers_received_count > 0
  96. return nil if rank < 1 || rank > 3
  97. certificate_type = "flower_top#{rank}".to_sym
  98. return nil if exists?(user: enrollment.user, reading_event: enrollment.reading_event, certificate_type: certificate_type)
  99. certificate_number = generate_certificate_number('FLOWER')
  100. achievement_data = build_flower_achievement_data(enrollment, rank)
  101. create!(
  102. user: enrollment.user,
  103. reading_event: enrollment.reading_event,
  104. certificate_type: certificate_type,
  105. certificate_number: certificate_number,
  106. issued_at: Time.current,
  107. achievement_data: achievement_data.to_json,
  108. is_public: true
  109. )
  110. end
  111. def self.generate_custom_certificate(enrollment, custom_data = {})
  112. certificate_number = generate_certificate_number('CUSTOM')
  113. achievement_data = build_custom_achievement_data(enrollment, custom_data)
  114. create!(
  115. user: enrollment.user,
  116. reading_event: enrollment.reading_event,
  117. certificate_type: :custom,
  118. certificate_number: certificate_number,
  119. issued_at: Time.current,
  120. achievement_data: achievement_data.to_json,
  121. is_public: custom_data[:is_public] != false
  122. )
  123. end
  124. # 证书内容方法
  125. def certificate_title
  126. case certificate_type
  127. when 'completion'
  128. "#{reading_event.title} 完成证书"
  129. when 'flower_top1'
  130. "#{reading_event.title} 小红花冠军证书"
  131. when 'flower_top2'
  132. "#{reading_event.title} 小红花亚军证书"
  133. when 'flower_top3'
  134. "#{reading_event.title} 小红花季军证书"
  135. when 'custom'
  136. "#{reading_event.title} 荣誉证书"
  137. end
  138. end
  139. def certificate_description
  140. case certificate_type
  141. when 'completion'
  142. "完成#{reading_event.days_count}天共读活动,完成率达到#{enrollment.completion_rate}%"
  143. when 'flower_top1'
  144. "在#{reading_event.title}活动中获得小红花数量第一名(#{enrollment.flowers_received_count}朵)"
  145. when 'flower_top2'
  146. "在#{reading_event.title}活动中获得小红花数量第二名(#{enrollment.flowers_received_count}朵)"
  147. when 'flower_top3'
  148. "在#{reading_event.title}活动中获得小红花数量第三名(#{enrollment.flowers_received_count}朵)"
  149. when 'custom'
  150. achievement_data['description'] || "在#{reading_event.title}活动中表现优异"
  151. end
  152. end
  153. def achievement_info
  154. return {} unless achievement_data.is_a?(String) || achievement_data.is_a?(Hash)
  155. data = achievement_data.is_a?(String) ? JSON.parse(achievement_data) : achievement_data
  156. data.with_indifferent_access
  157. end
  158. def enrollment
  159. @enrollment ||= reading_event.event_enrollments.find_by(user: user)
  160. end
  161. # 分享方法
  162. def shareable_url
  163. # 生成证书分享链接
  164. "/certificates/#{certificate_number}"
  165. end
  166. def shareable_image_url
  167. # 生成证书图片URL
  168. certificate_url || "/certificates/#{certificate_number}/image"
  169. end
  170. def can_share?
  171. is_public? && certificate_url.present?
  172. end
  173. # 验证方法
  174. def verify_certificate
  175. {
  176. valid: true,
  177. certificate_number: certificate_number,
  178. holder_name: user.nickname,
  179. event_name: reading_event.title,
  180. issue_date: issued_at.strftime('%Y年%m月%d日'),
  181. certificate_type: certificate_type,
  182. verification_code: generate_verification_code
  183. }
  184. end
  185. private
  186. # 证书编号生成
  187. def self.generate_certificate_number(prefix)
  188. timestamp = Time.current.strftime('%Y%m%d')
  189. random = SecureRandom.hex(4).upcase
  190. "#{prefix}-#{timestamp}-#{random}"
  191. end
  192. # 成就数据构建
  193. def self.build_completion_achievement_data(enrollment)
  194. {
  195. completion_rate: enrollment.completion_rate,
  196. check_ins_count: enrollment.check_ins_count,
  197. leader_days_count: enrollment.leader_days_count,
  198. flowers_received_count: enrollment.flowers_received_count,
  199. event_duration: enrollment.reading_event.days_count,
  200. book_name: enrollment.reading_event.book_name,
  201. activity_mode: enrollment.reading_event.activity_mode,
  202. issue_date: Time.current.iso8601
  203. }
  204. end
  205. def self.build_flower_achievement_data(enrollment, rank)
  206. {
  207. rank: rank,
  208. flowers_count: enrollment.flowers_received_count,
  209. completion_rate: enrollment.completion_rate,
  210. check_ins_count: enrollment.check_ins_count,
  211. book_name: enrollment.reading_event.book_name,
  212. total_participants: enrollment.reading_event.participants_count,
  213. issue_date: Time.current.iso8601
  214. }
  215. end
  216. def self.build_custom_achievement_data(enrollment, custom_data)
  217. {
  218. description: custom_data[:description],
  219. custom_fields: custom_data[:custom_fields] || {},
  220. completion_rate: enrollment.completion_rate,
  221. check_ins_count: enrollment.check_ins_count,
  222. flowers_received_count: enrollment.flowers_received_count,
  223. book_name: enrollment.reading_event.book_name,
  224. issue_date: Time.current.iso8601
  225. }
  226. end
  227. # 验证方法
  228. def certificate_number_format
  229. return unless certificate_number
  230. unless certificate_number.match?(/\A[A-Z]+-\d{8}-[A-F0-9]{8}\z/)
  231. errors.add(:certificate_number, "格式不正确")
  232. end
  233. end
  234. def user_must_be_event_participant
  235. return unless user && reading_event
  236. unless reading_event.participants.include?(user)
  237. errors.add(:user, "不是该活动的参与者")
  238. end
  239. end
  240. def certificate_requirements_met
  241. return unless user && reading_event && certificate_type
  242. case certificate_type
  243. when 'completion'
  244. unless enrollment&.is_completed?
  245. errors.add(:base, "用户未达到完成证书的颁发条件")
  246. end
  247. when 'flower_top1', 'flower_top2', 'flower_top3'
  248. unless enrollment&.flowers_received_count&.positive?
  249. errors.add(:base, "用户未达到小红花证书的颁发条件")
  250. end
  251. end
  252. end
  253. def generate_verification_code
  254. # 生成验证码用于证书验证
  255. Digest::MD5.hexdigest("#{certificate_number}-#{user_id}-#{reading_event_id}")[0, 8].upcase
  256. end
  257. end

app/models/post.rb

79.52% lines covered

83 relevant lines. 66 lines covered and 17 lines missed.
    
  1. 1 class Post < ApplicationRecord
  2. 1 belongs_to :user, counter_cache: :posts_count
  3. 1 has_many :comments, dependent: :destroy, counter_cache: true
  4. 1 has_many :likes, as: :target, dependent: :destroy
  5. # 验证
  6. 1 validates :title, presence: true, length: { maximum: 100 }
  7. 1 validates :content, presence: true, length: { minimum: 10, maximum: 5000 }
  8. 1 validates :category, inclusion: { in: %w[reading activity chat help], allow_blank: true }
  9. # 回调:手动维护多态关联的counter_cache
  10. 1 after_create :initialize_counters
  11. # 作用域
  12. 2 scope :visible, -> { where(hidden: false) }
  13. 2 scope :pinned_first, -> { order(pinned: :desc, created_at: :desc) }
  14. 1 scope :by_category, ->(category) { where(category: category) if category.present? }
  15. # 权限检查方法
  16. 1 def can_edit?(current_user)
  17. 7 return false unless current_user
  18. 6 return true if current_user.any_admin? # 管理员可以编辑任何帖子
  19. 3 return true if user_id == current_user.id # 作者可以编辑自己的帖子
  20. 2 false
  21. end
  22. 1 def can_hide?(current_user)
  23. 4 current_user&.any_admin?
  24. end
  25. 1 def can_pin?(current_user)
  26. 4 current_user&.any_admin?
  27. end
  28. # 管理员操作方法
  29. 1 def hide!
  30. 1 update!(hidden: true)
  31. end
  32. 1 def unhide!
  33. 1 update!(hidden: false)
  34. end
  35. 1 def pin!
  36. 1 update!(pinned: true)
  37. end
  38. 1 def unpin!
  39. 1 update!(pinned: false)
  40. end
  41. # 公共辅助方法
  42. 1 def can_edit_current_user
  43. # 这个方法会在控制器中设置
  44. 4 @can_edit_current_user || false
  45. end
  46. 1 def time_ago
  47. 8 time_ago_in_words(created_at)
  48. end
  49. 1 def time_ago_in_words(time)
  50. 8 seconds = Time.current - time
  51. 8 minutes = seconds / 60
  52. 8 hours = minutes / 60
  53. 8 days = hours / 24
  54. 8 if days >= 1
  55. 1 "#{days.to_i}天前"
  56. 7 elsif hours >= 1
  57. 1 "#{hours.to_i}小时前"
  58. 6 elsif minutes >= 1
  59. 1 "#{minutes.to_i}分钟前"
  60. else
  61. 5 "刚刚"
  62. end
  63. end
  64. # 获取分类名称
  65. 1 def category_name
  66. category_map = {
  67. 4 'reading' => '读书心得',
  68. 'activity' => '活动讨论',
  69. 'chat' => '闲聊区',
  70. 'help' => '求助问答'
  71. }
  72. 4 category_map[category] || '全部'
  73. end
  74. # 统计点赞数 - 使用counter_cache
  75. # 注意:需要手动维护多态关联的counter_cache
  76. 1 def likes_count
  77. 110 self[:likes_count] || likes.count
  78. end
  79. # 统计评论数 - 使用counter_cache
  80. 1 def comments_count
  81. 110 self[:comments_count] || comments.count
  82. end
  83. # 检查当前用户是否点赞
  84. 1 def liked_by?(current_user)
  85. return false unless current_user
  86. likes.exists?(user_id: current_user.id)
  87. end
  88. # 检查当前用户是否点赞(用于JSON序列化)
  89. 1 def liked_by_current_user
  90. 4 current_user = @current_user
  91. 4 return false unless current_user
  92. liked_by?(current_user)
  93. end
  94. # API序列化方法 - 标准化API响应格式
  95. 1 def as_json_for_api(options = {})
  96. current_user = options[:current_user]
  97. result = {
  98. id: id,
  99. title: title,
  100. content: content,
  101. category: category,
  102. category_name: category_name,
  103. pinned: pinned,
  104. hidden: hidden,
  105. created_at: created_at,
  106. updated_at: updated_at,
  107. time_ago: time_ago_in_words(created_at),
  108. stats: {
  109. likes_count: likes_count,
  110. comments_count: comments_count
  111. },
  112. author: user.as_json_for_api
  113. }
  114. # 添加标签信息
  115. if options[:include_tags] && respond_to?(:tags)
  116. result[:tags] = tags
  117. end
  118. # 添加当前用户的交互状态
  119. if current_user
  120. result[:interactions] = {
  121. liked: liked_by?(current_user),
  122. can_edit: can_edit?(current_user),
  123. can_hide: can_hide?(current_user),
  124. can_pin: can_pin?(current_user)
  125. }
  126. end
  127. # 包含关联数据
  128. if options[:include_comments]
  129. result[:recent_comments] = comments.limit(5).map(&:as_json_for_api)
  130. end
  131. if options[:include_likes]
  132. result[:recent_likes] = likes.limit(10).includes(:user).map do |like|
  133. {
  134. id: like.id,
  135. user: like.user.as_json_for_api,
  136. created_at: like.created_at
  137. }
  138. end
  139. end
  140. result
  141. end
  142. # JSON 序列化方法 - 保持向后兼容
  143. 1 def as_json(options = {})
  144. 4 super({
  145. methods: [:author_info, :can_edit_current_user, :time_ago, :category_name, :likes_count, :comments_count, :tags, :liked_by_current_user],
  146. include: {
  147. user: {
  148. only: [:id, :nickname, :avatar_url]
  149. }
  150. }
  151. }.merge(options))
  152. end
  153. 1 private
  154. 1 def author_info
  155. {
  156. 4 id: user.id,
  157. nickname: user.nickname,
  158. avatar_url: user.avatar_url,
  159. role: user.role_display_name
  160. }
  161. end
  162. # 初始化计数器
  163. 1 def initialize_counters
  164. # 新帖子初始化为0
  165. 102 update_column(:likes_count, 0) if likes_count.nil?
  166. 102 update_column(:comments_count, 0) if comments_count.nil?
  167. end
  168. # 手动更新点赞计数器
  169. 1 def increment_likes_count
  170. increment!(:likes_count)
  171. end
  172. 1 def decrement_likes_count
  173. decrement!(:likes_count)
  174. end
  175. end

app/models/reading_event.rb

45.43% lines covered

328 relevant lines. 149 lines covered and 179 lines missed.
    
  1. 1 class ReadingEvent < ApplicationRecord
  2. # 活动状态枚举
  3. 1 enum :status, {
  4. draft: 0, # 草稿
  5. enrolling: 1, # 报名中
  6. in_progress: 2, # 进行中
  7. completed: 3 # 已完成
  8. }, default: :draft
  9. # 审批状态枚举
  10. 1 enum :approval_status, {
  11. pending: 0, # 待审批
  12. approved: 1, # 已批准
  13. rejected: 2 # 已拒绝
  14. }, default: :pending
  15. # 活动模式枚举
  16. 1 enum :activity_mode, {
  17. note_checkin: 'note_checkin', # 笔记打卡
  18. free_discussion: 'free_discussion', # 自由讨论
  19. video_conference: 'video_conference', # 视频会议
  20. offline_meeting: 'offline_meeting' # 线下交流
  21. }, default: :note_checkin
  22. # 领读方式枚举
  23. 1 enum :leader_assignment_type, {
  24. voluntary: 'voluntary', # 自由领读
  25. random: 'random', # 随机领读
  26. disabled: 'disabled' # 无领读
  27. }, default: :voluntary
  28. # 费用类型枚举
  29. 1 enum :fee_type, {
  30. free: 'free', # 免费
  31. deposit: 'deposit', # 押金制
  32. paid: 'paid' # 收费制
  33. }, default: :free
  34. # 关联关系
  35. 1 belongs_to :leader, class_name: 'User', foreign_key: :leader_id
  36. 1 belongs_to :approver, class_name: 'User', foreign_key: :approved_by_id, optional: true
  37. 1 belongs_to :escalated_by, class_name: 'User', foreign_key: :escalated_by_user_id, optional: true
  38. 1 has_many :event_enrollments, dependent: :destroy, class_name: 'EventEnrollment'
  39. 1 has_many :participants, through: :event_enrollments, source: :user
  40. 1 has_many :reading_schedules, dependent: :destroy
  41. 1 has_many :participation_certificates, dependent: :destroy
  42. # 验证规则
  43. 1 validates :title, presence: true, length: { minimum: 5, maximum: 100 }
  44. 1 validates :book_name, presence: true, length: { minimum: 2, maximum: 100 }
  45. 1 validates :start_date, :end_date, presence: true
  46. 1 validates :max_participants, numericality: {
  47. greater_than: 0,
  48. less_than_or_equal_to: 50
  49. }
  50. 1 validates :min_participants, numericality: {
  51. greater_than: 0,
  52. 63 less_than_or_equal_to: ->(event) { event.max_participants || 50 }
  53. }
  54. 1 validates :fee_amount, numericality: {
  55. greater_than_or_equal_to: 0,
  56. less_than_or_equal_to: 500
  57. }
  58. 1 validates :leader_reward_percentage, numericality: {
  59. greater_than_or_equal_to: 0,
  60. less_than_or_equal_to: 100
  61. }
  62. 1 validates :completion_standard, numericality: {
  63. greater_than_or_equal_to: 60,
  64. less_than_or_equal_to: 100
  65. }
  66. 1 validate :end_date_after_start_date
  67. 1 validate :enrollment_deadline_before_start_date, if: :enrollment_deadline?
  68. 1 validate :min_participants_not_greater_than_max
  69. # 作用域
  70. 1 scope :with_details, -> { includes(:leader, :reading_schedules, :event_enrollments => :user) }
  71. 1 scope :filter_by_status, ->(status) { where(status: status) if status.present? }
  72. 1 scope :filter_by_mode, ->(mode) { where(activity_mode: mode) if mode.present? }
  73. 1 scope :filter_by_fee_type, ->(fee_type) { where(fee_type: fee_type) if fee_type.present? }
  74. 1 scope :upcoming, -> { where('start_date > ?', Date.current) }
  75. 1 scope :active, -> { where(status: [:enrolling, :in_progress]) }
  76. 1 scope :enrolling, -> { where(status: :enrolling) }
  77. 1 scope :in_progress, -> { where(status: :in_progress) }
  78. 1 scope :completed, -> { where(status: :completed) }
  79. # 委托方法
  80. 1 delegate :nickname, to: :leader, prefix: true
  81. # 计算方法
  82. 1 def service_fee
  83. 1 fee_amount * 0.2
  84. end
  85. 1 def deposit
  86. 1 fee_amount * 0.8
  87. end
  88. 1 def days_count
  89. 3 return 0 unless start_date && end_date
  90. 1 (end_date - start_date).to_i + 1
  91. end
  92. # 审批相关方法
  93. 1 def approve!(admin_user)
  94. 1 update!(
  95. approval_status: :approved,
  96. approved_by_id: admin_user.id,
  97. approved_at: Time.current
  98. )
  99. end
  100. 1 def reject!(admin_user, reason = nil)
  101. 1 update!(
  102. approval_status: :rejected,
  103. approved_by_id: admin_user.id,
  104. approved_at: Time.current,
  105. rejection_reason: reason
  106. )
  107. end
  108. 1 def approved?
  109. 7 approval_status == 'approved'
  110. end
  111. 1 def pending_approval?
  112. 1 approval_status == 'pending'
  113. end
  114. 1 def rejected?
  115. 1 approval_status == 'rejected'
  116. end
  117. # 审批工作流相关方法
  118. 1 def can_submit_for_approval?
  119. draft? && !submitted_for_approval_at.present?
  120. end
  121. 1 def can_resubmit_for_approval?
  122. rejected? && rejection_reason.present?
  123. end
  124. 1 def can_be_approved_by?(admin_user)
  125. pending_approval? && admin_user.can_approve_events?
  126. end
  127. 1 def can_be_rejected_by?(admin_user)
  128. pending_approval? && admin_user.can_approve_events?
  129. end
  130. 1 def submit_for_approval!(workflow_type = :standard)
  131. return false unless can_submit_for_approval?
  132. service = ActivityApprovalWorkflowService.submit_for_approval!(self, workflow_type: workflow_type)
  133. service.success?
  134. end
  135. 1 def process_approval!(admin_user, reason: nil, notes: nil)
  136. return false unless can_be_approved_by?(admin_user)
  137. service = ActivityApprovalWorkflowService.approve!(self, admin_user, reason: reason, notes: notes)
  138. service.success?
  139. end
  140. 1 def process_rejection!(admin_user, reason, notes: nil)
  141. return false unless can_be_rejected_by?(admin_user)
  142. service = ActivityApprovalWorkflowService.reject!(self, admin_user, reason, notes: notes)
  143. service.success?
  144. end
  145. 1 def escalate_approval!(admin_user, escalation_reason)
  146. service = ActivityApprovalWorkflowService.escalate!(self, admin_user, escalation_reason)
  147. service.success?
  148. end
  149. # 领读人分配方法
  150. 1 def assign_daily_leaders!(assignment_type = nil, options = {})
  151. 3 return unless approved? && reading_schedules.any?
  152. # 使用增强的LeaderAssignmentService
  153. 3 service = LeaderAssignmentService.auto_assign_leaders!(self, assignment_type: assignment_type, options: options)
  154. 3 service.success?
  155. end
  156. 1 def assign_random_leaders!
  157. 1 assign_daily_leaders!(:random)
  158. end
  159. # 活动完成时重置所有角色
  160. 1 def complete_event!
  161. 1 transaction do
  162. 1 update!(status: :completed)
  163. # 重置所有参与者的角色
  164. 1 enrollments.each do |enrollment|
  165. enrollment.reset_roles_on_event_completion!
  166. end
  167. # 生成活动总结(可选)
  168. generate_event_summary
  169. end
  170. end
  171. # 检查当前用户是否是有效的小组长
  172. 1 def current_leader?(user)
  173. 9 return false unless in_progress?
  174. 5 leader_id == user.id
  175. end
  176. # 检查当前用户是否是有效的领读人(3天权限窗口)
  177. 1 def current_daily_leader?(user, schedule = nil)
  178. 1 return false unless in_progress?
  179. 1 if schedule.present?
  180. return false unless schedule.reading_event_id == id
  181. return false unless schedule.daily_leader_id == user.id
  182. # 3天权限窗口:前一天、当天、后一天
  183. leader_date = schedule.date
  184. today = Date.today
  185. # 检查今天是否在领读人的权限窗口内
  186. (leader_date - 1.day) <= today && today <= (leader_date + 1.day)
  187. else
  188. # 查找用户作为领读人的所有schedule,检查是否在权限窗口内
  189. 1 user_schedules = reading_schedules.where(daily_leader: user)
  190. 1 return false if user_schedules.empty?
  191. user_schedules.any? do |schedule|
  192. leader_date = schedule.date
  193. today = Date.today
  194. (leader_date - 1.day) <= today && today <= (leader_date + 1.day)
  195. end
  196. end
  197. end
  198. # 检查用户是否有权限发布领读内容(前一天权限 + 小组长补位)
  199. 1 def can_publish_leading_content?(user, schedule)
  200. 1 return false unless in_progress?
  201. 1 return false unless schedule.reading_event_id == id
  202. # 小组长全程具备发布权限(补位机制)
  203. 1 return true if current_leader?(user)
  204. # 领读人权限检查
  205. return false unless schedule.daily_leader_id == user.id
  206. # 允许前一天发布领读内容
  207. schedule.date >= Date.today
  208. end
  209. # 检查用户是否有权限发放小红花(当天和后一天权限 + 小组长补位)
  210. 1 def can_give_flowers?(user, schedule)
  211. 1 return false unless in_progress?
  212. # 小组长全程具备发小红花权限(补位机制)
  213. 1 return true if current_leader?(user)
  214. # 领读人权限检查
  215. user_leading_schedules = reading_schedules.where(daily_leader: user)
  216. return false if user_leading_schedules.empty?
  217. # 检查是否有schedule在小红花发放权限窗口内
  218. leader_dates = user_leading_schedules.pluck(:date)
  219. today = Date.today
  220. leader_dates.any? do |leader_date|
  221. # 当天和后一天可以发小红花
  222. leader_date <= today && today <= (leader_date + leader_flower_grace_period.days)
  223. end
  224. end
  225. # 检查领读人是否缺失工作
  226. 1 def missing_leader_work?(date = Date.today)
  227. return false unless in_progress?
  228. schedule = reading_schedules.find_by(date: date)
  229. return false unless schedule&.daily_leader.present?
  230. # 检查是否缺失领读内容
  231. missing_content = !schedule.daily_leading.present?
  232. # 检查是否缺失小红花(如果是前一天或前两天的领读)
  233. missing_flowers = false
  234. if date <= Date.today && date >= Date.today - 2.days
  235. schedule_date = date
  236. flower_window_end = schedule_date + leader_flower_grace_period.days
  237. if Date.today <= flower_window_end
  238. # 还在小红花发放窗口内,检查是否已发放
  239. check_ins_count = schedule.check_ins.count
  240. flowers_count = schedule.flowers.count
  241. # 有打卡但没有足够的小红花(建议至少1朵)
  242. missing_flowers = check_ins_count > 0 && flowers_count == 0
  243. end
  244. end
  245. {
  246. schedule: schedule,
  247. missing_content: missing_content,
  248. missing_flowers: missing_flowers,
  249. leader: schedule.daily_leader,
  250. needs_backup: missing_content || missing_flowers
  251. }
  252. end
  253. # 获取需要补位的日程列表
  254. 1 def schedules_need_backup
  255. 1 return [] unless in_progress?
  256. # 检查最近3天的日程
  257. 1 date_range = (Date.today - 1.day)..(Date.today + 1.day)
  258. 1 schedules = reading_schedules.where(date: date_range).includes(:daily_leader, :daily_leading, :flowers, :check_ins)
  259. 1 backup_needed = []
  260. 1 schedules.each do |schedule|
  261. # 检查领读内容是否缺失
  262. content_missing = schedule.daily_leader.present? && !schedule.daily_leading.present?
  263. # 检查小红花是否缺失
  264. flowers_missing = false
  265. if schedule.date <= Date.today && schedule.check_ins.any?
  266. # 已经有打卡但没有小红花
  267. flowers_missing = schedule.flowers.empty?
  268. end
  269. if content_missing || flowers_missing
  270. backup_needed << {
  271. schedule: schedule,
  272. date: schedule.date,
  273. day_number: schedule.day_number,
  274. leader: schedule.daily_leader,
  275. missing_content: content_missing,
  276. missing_flowers: flowers_missing,
  277. content_deadline: schedule.date,
  278. flowers_deadline: schedule.date + leader_flower_grace_period.days
  279. }
  280. end
  281. end
  282. 1 backup_needed
  283. end
  284. # 获取领读人权限窗口配置(可配置化)
  285. 1 def leader_permission_window
  286. 1 {
  287. content_publish_days_before: 1, # 提前1天可以发布内容
  288. content_publish_days_after: 0, # 当天后不能发布内容
  289. flower_give_days_before: 0, # 当天前不能发小红花
  290. flower_give_days_after: 1 # 当天后1天可以发小红花
  291. }
  292. end
  293. # 统一的枚举访问方法 - 公有方法供Service使用
  294. 1 def status_symbol
  295. status.to_sym
  296. end
  297. 1 def approval_status_symbol
  298. approval_status.to_sym
  299. end
  300. 1 def leader_assignment_type_symbol
  301. leader_assignment_type.to_sym
  302. end
  303. # 设置时也接受符号(可选,用于一致性)
  304. 1 def status_symbol=(value)
  305. self.status = value.to_s
  306. end
  307. 1 def approval_status_symbol=(value)
  308. self.approval_status = value.to_s
  309. end
  310. 1 def leader_assignment_type_symbol=(value)
  311. self.leader_assignment_type = value.to_s
  312. end
  313. # 状态方法
  314. 1 def can_start?
  315. enrolling? && start_date <= Date.current && enough_participants?
  316. end
  317. 1 def can_enroll?
  318. enrolling? && (enrollment_deadline.blank? || enrollment_deadline > Time.current) && !max_participants_reached?
  319. end
  320. # 报名相关辅助方法
  321. 1 def enrollment_error_message
  322. return "活动不在报名状态" unless enrolling?
  323. return "报名已截止" if enrollment_deadline.present? && enrollment_deadline <= Time.current
  324. return "活动人数已满" if max_participants_reached?
  325. return "活动尚未批准" unless approved?
  326. "无法报名"
  327. end
  328. 1 def user_enrolled?(user)
  329. return false unless user
  330. event_enrollments.where(user: user, status: 'enrolled').exists?
  331. end
  332. 1 def user_enrollment(user)
  333. return nil unless user
  334. event_enrollments.find_by(user: user)
  335. end
  336. 1 def enrollment_statistics
  337. enrollments = event_enrollments.includes(:user)
  338. {
  339. total_enrollments: enrollments.count,
  340. active_enrollments: enrollments.where(status: 'enrolled').count,
  341. completed_enrollments: enrollments.where(status: 'completed').count,
  342. cancelled_enrollments: enrollments.where(status: 'cancelled').count,
  343. participants_count: enrollments.where(enrollment_type: 'participant').count,
  344. observers_count: enrollments.where(enrollment_type: 'observer').count,
  345. total_fees_collected: enrollments.sum(:fee_paid_amount),
  346. total_refunds_processed: enrollments.sum(:fee_refund_amount),
  347. enrollment_rate: calculate_enrollment_rate,
  348. completion_rate: calculate_overall_completion_rate(enrollments)
  349. }
  350. end
  351. 1 def start!
  352. return false unless can_start?
  353. ActiveRecord::Base.transaction do
  354. update!(status: :in_progress)
  355. # 生成阅读计划(如果还没有)
  356. generate_reading_schedules if reading_schedules.empty?
  357. # 分配领读人
  358. assign_daily_leaders! if leader_assignment_type != 'disabled'
  359. true
  360. end
  361. end
  362. 1 def complete!
  363. return false unless can_complete?
  364. ActiveRecord::Base.transaction do
  365. update!(status: :completed)
  366. # 处理所有未完成的报名
  367. event_enrollments.where(status: 'enrolled').each do |enrollment|
  368. enrollment.update_completion_rate!
  369. end
  370. # 生成完成证书
  371. generate_completion_certificates
  372. true
  373. end
  374. end
  375. 1 def can_complete?
  376. in_progress? && end_date < Date.current
  377. end
  378. 1 def max_participants_reached?
  379. event_enrollments.enrolled.count >= max_participants
  380. end
  381. 1 def enough_participants?
  382. event_enrollments.enrolled.count >= min_participants
  383. end
  384. 1 def participants_count
  385. event_enrollments.enrolled.count
  386. end
  387. 1 def available_spots
  388. max_participants - participants_count
  389. end
  390. # 统计方法
  391. 1 def completion_statistics
  392. enrollments = event_enrollments.includes(:user)
  393. {
  394. total_participants: enrollments.count,
  395. completed_participants: enrollments.where('completion_rate >= ?', completion_standard).count,
  396. average_completion_rate: enrollments.average(:completion_rate)&.round(2) || 0,
  397. total_check_ins: enrollments.sum(:check_ins_count),
  398. total_flowers: enrollments.sum(:flowers_received_count)
  399. }
  400. end
  401. # 费用计算方法
  402. 1 def calculate_leader_reward
  403. return 0 if fee_type == 'free'
  404. if fee_type == 'deposit'
  405. fee_amount * (leader_reward_percentage / 100.0) * participants_count
  406. else # paid
  407. fee_amount * participants_count
  408. end
  409. end
  410. 1 def calculate_deposit_pool
  411. return 0 if fee_type != 'deposit'
  412. total_fees = fee_amount * participants_count
  413. leader_reward = calculate_leader_reward
  414. total_fees - leader_reward
  415. end
  416. # 验证活动是否满足审批条件(公开方法供Service使用)
  417. 1 def validate_event_for_approval
  418. errors = []
  419. # 检查基本信息
  420. errors << "活动标题不能为空" if title.blank?
  421. errors << "活动描述不能为空" if description.blank?
  422. errors << "书籍名称不能为空" if book_name.blank?
  423. # 检查日期设置
  424. errors << "开始日期不能为空" if start_date.blank?
  425. errors << "结束日期不能为空" if end_date.blank?
  426. errors << "开始日期必须在今天之后" if start_date <= Date.today
  427. # 检查人数设置
  428. errors << "最大参与人数必须大于0" if max_participants.nil? || max_participants <= 0
  429. errors << "最小参与人数不能大于最大参与人数" if min_participants > max_participants
  430. # 检查费用设置(如果是收费活动)
  431. if fee_type != 'free'
  432. errors << "收费活动必须设置费用金额" if fee_amount.nil? || fee_amount <= 0
  433. errors << "收费活动必须设置领读人奖励比例" if leader_reward_percentage.nil?
  434. end
  435. # 检查阅读计划
  436. if reading_schedules.empty?
  437. errors << "必须设置阅读计划"
  438. end
  439. # 检查特定活动模式的特殊要求
  440. case activity_mode
  441. when 'video_conference'
  442. errors << "视频会议活动必须设置会议链接" if meeting_link.blank?
  443. when 'offline_meeting'
  444. errors << "线下活动必须设置活动地点" if location.blank?
  445. end
  446. {
  447. valid: errors.empty?,
  448. errors: errors
  449. }
  450. end
  451. # 小红花统计
  452. 1 def flowers_count
  453. Flower.joins(check_in: :event_enrollment)
  454. .where(event_enrollments: { reading_event_id: id })
  455. .count
  456. end
  457. 1 def flowers_given_count
  458. Flower.joins(check_in: :event_enrollment)
  459. .where(event_enrollments: { reading_event_id: id })
  460. .count
  461. end
  462. # 小红花配额
  463. 1 has_many :flower_quotas, dependent: :destroy
  464. # 小红花证书
  465. 1 has_many :flower_certificates, dependent: :destroy
  466. # 获取活动中的小红花排行榜(前三名)
  467. 1 def flower_top_three
  468. flower_stats = Flower.joins(:recipient)
  469. .joins(check_in: :event_enrollment)
  470. .where(event_enrollments: { reading_event_id: id })
  471. .group('recipients.id')
  472. .sum(:amount)
  473. flower_stats.sort_by { |user_id, flowers| -flowers }
  474. .first(3)
  475. .map.with_index(1) { |(user_id, flowers), index| [User.find(user_id), flowers, index] }
  476. end
  477. # 检查用户是否有剩余配额
  478. 1 def user_has_remaining_flower_quota?(user)
  479. return false unless participants.include?(user)
  480. quota = FlowerQuota.get_or_create_quota(user, self)
  481. quota.can_give_flower?
  482. end
  483. # 活动结束时自动生成证书
  484. 1 def generate_flower_certificates_if_completed
  485. return unless status == 'completed'
  486. FlowerCertificate.generate_top_three_certificates(self)
  487. end
  488. # 检查用户是否在活动中有剩余小红花配额
  489. 1 def user_has_remaining_flower_quota?(user)
  490. return false unless participants.include?(user)
  491. quota = FlowerQuota.get_or_create_quota(user, self)
  492. quota.can_give_flower?
  493. end
  494. # 获取用户在活动中的配额信息
  495. 1 def user_flower_quota_info(user)
  496. return nil unless participants.include?(user)
  497. FlowerIncentiveService.get_user_quota_info(user, self)
  498. end
  499. # 获取活动的小红花激励统计
  500. 1 def flower_incentive_statistics
  501. return { error: '活动未结束' } unless status == 'completed'
  502. certificates = FlowerCertificate.for_event(self).ranked
  503. total_flowers_given = Flower.joins(:recipient)
  504. .joins(check_in: :event_enrollment)
  505. .where(event_enrollments: { reading_event_id: id })
  506. .sum(:amount)
  507. {
  508. event: title,
  509. status: status,
  510. total_participants: participants.count,
  511. total_flowers_given: total_flowers_given,
  512. certificates_generated: certificates.count,
  513. top_three_winners: certificates.map do |cert|
  514. {
  515. rank: cert.rank_display,
  516. user: cert.user.as_json_for_api,
  517. total_flowers: cert.total_flowers,
  518. honor_level: cert.honor_level,
  519. certificate_id: cert.certificate_id
  520. }
  521. end,
  522. generated_at: certificates.first&.created_at
  523. }
  524. end
  525. # 活动结束时生成小红花总结和证书
  526. 1 def finalize_flower_incentives
  527. return { error: '活动未结束' } unless status == 'completed'
  528. return { error: '活动没有参与者' } if participants.empty?
  529. # 生成证书
  530. result = FlowerIncentiveService.finalize_event_flower_certificates(self)
  531. # 生成活动总结
  532. summary = {
  533. event: title,
  534. duration: "#{start_date} 至 #{end_date}",
  535. participants_count: participants.count,
  536. certificates_generated: result[:certificates]&.count || 0,
  537. total_flowers_given: flowers_count,
  538. top_three: result[:certificates]&.map do |cert|
  539. {
  540. rank: cert[:rank_display],
  541. user: cert[:user],
  542. total_flowers: cert[:total_flowers]
  543. }
  544. end
  545. }
  546. {
  547. success: true,
  548. summary: summary,
  549. certificates: result[:certificates]
  550. }
  551. end
  552. # API响应格式化
  553. 1 def as_json_for_api(options = {})
  554. base_data = {
  555. id: id,
  556. title: title,
  557. book_name: book_name,
  558. book_cover_url: book_cover_url,
  559. description: description,
  560. start_date: start_date,
  561. end_date: end_date,
  562. max_participants: max_participants,
  563. min_participants: min_participants,
  564. fee_type: fee_type,
  565. fee_amount: fee_amount,
  566. leader_reward_percentage: leader_reward_percentage,
  567. completion_standard: completion_standard,
  568. activity_mode: activity_mode,
  569. weekend_rest: weekend_rest,
  570. leader_assignment_type: leader_assignment_type,
  571. status: status,
  572. approval_status: approval_status,
  573. created_at: created_at,
  574. updated_at: updated_at
  575. }
  576. # 可选包含关联数据
  577. if options[:include_leader]
  578. base_data[:leader] = leader&.as_json_for_api
  579. end
  580. if options[:include_participants]
  581. base_data[:participants] = participants.map(&:as_json_for_api)
  582. end
  583. if options[:include_statistics]
  584. base_data[:statistics] = completion_statistics
  585. base_data[:enrollment_statistics] = enrollment_statistics
  586. end
  587. if options[:include_schedules]
  588. base_data[:reading_schedules] = reading_schedules.map do |schedule|
  589. {
  590. id: schedule.id,
  591. day_number: schedule.day_number,
  592. date: schedule.date,
  593. reading_progress: schedule.reading_progress
  594. }
  595. end
  596. end
  597. base_data
  598. end
  599. 1 private
  600. 1 def calculate_enrollment_rate
  601. return 0 if max_participants == 0
  602. (event_enrollments.enrolled.count.to_f / max_participants * 100).round(2)
  603. end
  604. 1 def calculate_overall_completion_rate(enrollments)
  605. return 0 if enrollments.empty?
  606. (enrollments.average(:completion_rate) || 0).round(2)
  607. end
  608. 1 def generate_reading_schedules
  609. return unless start_date && end_date
  610. (start_date..end_date).each_with_index do |date, index|
  611. next if weekend_rest && (date.saturday? || date.sunday?)
  612. reading_schedules.create!(
  613. day_number: index + 1,
  614. date: date,
  615. reading_pages: nil, # 可以根据需要设置默认阅读页数
  616. reading_content: nil
  617. )
  618. end
  619. end
  620. 1 def generate_completion_certificates
  621. event_enrollments.where(status: 'completed').each do |enrollment|
  622. next unless enrollment.is_completed?
  623. # 生成完成证书
  624. ParticipationCertificate.generate_completion_certificate(enrollment)
  625. # 检查小红花排名并生成相应证书
  626. flower_rank = get_flower_rank(enrollment)
  627. if flower_rank && flower_rank <= 3
  628. ParticipationCertificate.generate_flower_certificate(enrollment, flower_rank)
  629. end
  630. end
  631. end
  632. 1 def get_flower_rank(enrollment)
  633. return 0 if enrollment.flowers_received_count == 0
  634. rankings = event_enrollments
  635. .where('flowers_received_count > 0')
  636. .order(flowers_received_count: :desc)
  637. .pluck(:id)
  638. rankings.index(enrollment.id) + 1
  639. end
  640. 1 private
  641. 1 def leader_flower_grace_period
  642. leader_permission_window[:flower_give_days_after]
  643. end
  644. # 验证方法
  645. 1 def end_date_after_start_date
  646. 63 return if end_date.blank? || start_date.blank?
  647. 61 if end_date < start_date
  648. 1 errors.add(:end_date, "必须在开始日期之后")
  649. end
  650. end
  651. 1 def enrollment_deadline_before_start_date
  652. return if enrollment_deadline.blank? || start_date.blank?
  653. if enrollment_deadline > start_date.to_time
  654. errors.add(:enrollment_deadline, "必须在活动开始日期之前")
  655. end
  656. end
  657. 1 def min_participants_not_greater_than_max
  658. 63 return if min_participants.blank? || max_participants.blank?
  659. 63 if min_participants > max_participants
  660. 2 errors.add(:min_participants, "不能大于最大参与人数")
  661. end
  662. end
  663. 1 def can_be_enrolling?
  664. start_date > Date.current && approval_status == 'approved'
  665. end
  666. 1 def generate_event_summary
  667. # 这里可以实现活动总结的生成逻辑
  668. # 比如统计小红花排名、完成率等
  669. puts "活动【#{title}】已完成!"
  670. end
  671. end

app/models/reading_schedule.rb

50.0% lines covered

102 relevant lines. 51 lines covered and 51 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: reading_schedules
  4. #
  5. # id :integer not null, primary key
  6. # reading_event_id :integer not null
  7. # day_number :integer not null
  8. # date :date not null
  9. # reading_progress :string not null
  10. # daily_leader_id :integer
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. #
  14. # Indexes
  15. #
  16. # index_reading_schedules_on_date (date)
  17. # index_reading_schedules_on_daily_leader_id (daily_leader_id)
  18. # index_reading_schedules_on_reading_event_id (reading_event_id)
  19. # index_reading_schedules_on_reading_event_id_and_day_number (reading_event_id, day_number) UNIQUE
  20. #
  21. # Foreign Keys
  22. #
  23. # fk_rails_... (daily_leader_id => users.id)
  24. # fk_rails_... (reading_event_id => reading_events.id)
  25. #
  26. 1 class ReadingSchedule < ApplicationRecord
  27. # 关联关系
  28. 1 belongs_to :reading_event
  29. 1 belongs_to :daily_leader, class_name: 'User', optional: true
  30. 1 has_many :check_ins, dependent: :destroy
  31. 1 has_one :daily_leading, dependent: :destroy
  32. 1 has_many :flowers, dependent: :destroy
  33. # 验证规则
  34. 1 validates :day_number, presence: true, numericality: { greater_than: 0 }
  35. 1 validates :reading_progress, presence: true, length: { maximum: 200 }
  36. 1 validates :date, presence: true
  37. 1 validates_uniqueness_of :day_number, scope: :reading_event_id
  38. 1 validate :date_within_event_period
  39. 1 validate :leader_must_be_event_participant, if: :daily_leader_id?
  40. # 作用域
  41. 1 scope :today, -> { where(date: Date.current) }
  42. 1 scope :past, -> { where('date < ?', Date.current) }
  43. 1 scope :future, -> { where('date > ?', Date.current) }
  44. 1 scope :with_leader, -> { where.not(daily_leader_id: nil) }
  45. 1 scope :without_leader, -> { where(daily_leader_id: nil) }
  46. 1 scope :with_leading_content, -> { joins(:daily_leading) }
  47. 1 scope :chronological, -> { order(:day_number) }
  48. 1 scope :by_date, ->(direction = :asc) { order(date: direction) }
  49. # 委托方法
  50. 1 delegate :title, :activity_mode, :in_progress?, to: :reading_event, prefix: true
  51. # 状态方法
  52. 1 def today?
  53. date == Date.current
  54. end
  55. 1 def past?
  56. date < Date.current
  57. end
  58. 1 def future?
  59. date > Date.current
  60. end
  61. 1 def current_day?
  62. reading_event.in_progress? && (date == Date.current || (date < Date.current && !completed?))
  63. end
  64. 1 def can_assign_leader?
  65. daily_leader_id.blank? && (future? || current_day?)
  66. end
  67. 1 def can_publish_leading_content?
  68. # 领读人权限窗口:前一天可以发布内容
  69. return false unless daily_leader.present?
  70. permission_start = date - 1.day
  71. permission_end = date
  72. Date.current.between?(permission_start, permission_end)
  73. end
  74. 1 def can_give_flowers?
  75. # 小红花发放权限窗口:当天和后一天
  76. return false unless check_ins.any?
  77. permission_start = date
  78. permission_end = date + 1.day
  79. Date.current.between?(permission_start, permission_end)
  80. end
  81. 1 def has_leading_content?
  82. daily_leading.present?
  83. end
  84. 1 def has_check_ins?
  85. check_ins.exists?
  86. end
  87. 1 def has_flowers?
  88. flowers.exists?
  89. end
  90. 1 def completed?
  91. return true if past? && has_check_ins?
  92. return true if reading_event.completed?
  93. false
  94. end
  95. # 领读人分配方法
  96. 1 def assign_leader!(user)
  97. return false unless can_assign_leader?
  98. return false unless reading_event.participants.include?(user)
  99. transaction do
  100. update!(daily_leader: user)
  101. notify_leader_assignment(user)
  102. end
  103. true
  104. end
  105. 1 def remove_leader!
  106. return false unless daily_leader.present?
  107. transaction do
  108. update!(daily_leader: nil)
  109. # 删除相关的领读内容
  110. daily_leading&.destroy
  111. end
  112. true
  113. end
  114. # 统计方法
  115. 1 def participation_statistics
  116. {
  117. check_ins_count: check_ins.count,
  118. flowers_count: flowers.count,
  119. unique_participants: check_ins.distinct.count(:user_id),
  120. average_word_count: check_ins.average(:word_count)&.round(2) || 0
  121. }
  122. end
  123. 1 def leading_content_status
  124. return 'no_leader' if daily_leader.blank?
  125. return 'content_published' if has_leading_content?
  126. return 'content_pending' if can_publish_leading_content?
  127. 'content_overdue'
  128. end
  129. 1 def flower_giving_status
  130. return 'no_check_ins' unless has_check_ins?
  131. return 'flowers_given' if has_flowers?
  132. return 'flowers_pending' if can_give_flowers?
  133. 'flowers_overdue'
  134. end
  135. # 检查是否需要小组长补位
  136. 1 def needs_backup?
  137. return false unless reading_event.in_progress?
  138. # 检查领读内容是否缺失
  139. content_missing = daily_leader.present? && !has_leading_content? && !can_publish_leading_content?
  140. # 检查小红花是否缺失
  141. flowers_missing = has_check_ins? && !has_flowers? && !can_give_flowers?
  142. content_missing || flowers_missing
  143. end
  144. # 获取补位权限
  145. 1 def backup_permissions
  146. return {} unless reading_event.in_progress?
  147. {
  148. can_publish_content: reading_event.current_leader?(reading_event.leader),
  149. can_give_flowers: reading_event.current_leader?(reading_event.leader),
  150. content_deadline: date,
  151. flowers_deadline: date + 1.day
  152. }
  153. end
  154. # 通知方法
  155. 1 def notify_leader_assignment(leader)
  156. # 发送领读人分配通知
  157. LeaderAssignmentService.notify_assignment(self, leader)
  158. end
  159. 1 def notify_leading_content_published
  160. return unless daily_leader.present?
  161. # 发送领读内容发布通知
  162. LeaderAssignmentService.notify_content_published(self)
  163. end
  164. 1 def notify_check_in_submitted(check_in)
  165. # 发送打卡提交通知给领读人和小组长
  166. CheckInNotificationService.notify_submitted(check_in)
  167. end
  168. 1 def notify_flower_given(flower)
  169. # 发送小红花发放通知
  170. FlowerNotificationService.notify_given(flower)
  171. end
  172. 1 private
  173. # 验证方法
  174. 1 def date_within_event_period
  175. 18 return unless date && reading_event
  176. 18 if date < reading_event.start_date || date > reading_event.end_date
  177. errors.add(:date, "必须在活动时间范围内")
  178. end
  179. end
  180. 1 def leader_must_be_event_participant
  181. 7 return unless daily_leader_id && reading_event
  182. 7 unless reading_event.participants.include?(daily_leader)
  183. 7 errors.add(:daily_leader, "必须是活动的参与者")
  184. end
  185. end
  186. end

app/models/share_action.rb

0.0% lines covered

125 relevant lines. 0 lines covered and 125 lines missed.
    
  1. class ShareAction < ApplicationRecord
  2. # 关联
  3. belongs_to :user, optional: true
  4. # 验证
  5. validates :share_type, :resource_id, :platform, presence: true
  6. # 枚举
  7. enum :share_type, {
  8. daily_leaderboard: 'daily_leaderboard', # 每日排行榜
  9. final_leaderboard: 'final_leaderboard', # 最终排行榜
  10. certificate: 'certificate', # 证书分享
  11. user_achievement: 'user_achievement' # 用户成就
  12. }
  13. enum :platform, {
  14. wechat: 'wechat', # 微信
  15. weibo: 'weibo', # 微博
  16. qq: 'qq', # QQ
  17. copy_link: 'copy_link' # 复制链接
  18. }
  19. # 作用域
  20. scope :for_share_type, ->(type) { where(share_type: type) }
  21. scope :for_platform, ->(platform) { where(platform: platform) }
  22. scope :for_user, ->(user) { where(user: user) }
  23. scope :recent, -> { order(shared_at: :desc) }
  24. scope :today, -> { where(shared_at: Date.current.beginning_of_day..Date.current.end_of_day) }
  25. # 回调
  26. before_validation :set_shared_at, on: :create
  27. # 实例方法
  28. # 获取分享的资源对象
  29. def resource
  30. case share_type
  31. when 'daily_leaderboard'
  32. DailyFlowerStat.find_by(id: resource_id)
  33. when 'final_leaderboard'
  34. FlowerCertificate.for_event(ReadingEvent.find_by(id: resource_id))
  35. when 'certificate'
  36. FlowerCertificate.find_by(certificate_id: resource_id)
  37. when 'user_achievement'
  38. { user_id: user_id, event_id: resource_id }
  39. end
  40. end
  41. # 获取分享的显示名称
  42. def share_type_display
  43. case share_type
  44. when 'daily_leaderboard'
  45. '每日排行榜'
  46. when 'final_leaderboard'
  47. '最终排行榜'
  48. when 'certificate'
  49. '证书分享'
  50. when 'user_achievement'
  51. '个人成就'
  52. else
  53. share_type
  54. end
  55. end
  56. # 获取平台显示名称
  57. def platform_display
  58. case platform
  59. when 'wechat'
  60. '微信'
  61. when 'weibo'
  62. '微博'
  63. when 'qq'
  64. 'QQ'
  65. when 'copy_link'
  66. '复制链接'
  67. else
  68. platform
  69. end
  70. end
  71. # 检查是否为今日分享
  72. def shared_today?
  73. shared_at.to_date == Date.current
  74. end
  75. # API响应格式
  76. def as_json_for_api
  77. {
  78. id: id,
  79. share_type: share_type_display,
  80. platform: platform_display,
  81. resource_id: resource_id,
  82. user: user&.as_json_for_api,
  83. shared_at: shared_at,
  84. shared_today: shared_today?,
  85. ip_address: ip_address
  86. }
  87. end
  88. # 类方法
  89. # 获取分享统计
  90. def self.share_statistics(days: 7)
  91. start_date = days.days.ago.to_date
  92. stats = where('shared_at >= ?', start_date)
  93. .group(:share_type, :platform)
  94. .count
  95. {
  96. period: "#{start_date} 至 #{Date.current}",
  97. total_shares: stats.values.sum,
  98. share_type_breakdown: stats.group_by { |(type, _), _| type }
  99. .transform_values { |items| items.values.sum },
  100. platform_breakdown: stats.group_by { |(_, platform), _| platform }
  101. .transform_values(&:sum),
  102. detailed_stats: stats
  103. }
  104. end
  105. # 获取用户的分享历史
  106. def self.user_share_history(user, limit: 20)
  107. for_user(user)
  108. .recent
  109. .limit(limit)
  110. .includes(:user)
  111. end
  112. # 获取热门分享内容
  113. def self.popular_shares(days: 7, limit: 10)
  114. start_date = days.days.ago.to_date
  115. joins(:user)
  116. .where('shared_at >= ?', start_date)
  117. .group(:share_type, :resource_id)
  118. .order('COUNT(*) DESC')
  119. .limit(limit)
  120. .count
  121. end
  122. # 记录分享行为(便捷方法)
  123. def self.record_share(share_type:, resource_id:, platform:, user: nil, ip_address: nil, user_agent: nil)
  124. create!(
  125. share_type: share_type,
  126. resource_id: resource_id,
  127. platform: platform,
  128. user: user,
  129. ip_address: ip_address,
  130. user_agent: user_agent,
  131. shared_at: Time.current
  132. )
  133. rescue => e
  134. Rails.logger.error "记录分享行为失败: #{e.message}"
  135. nil
  136. end
  137. private
  138. def set_shared_at
  139. self.shared_at ||= Time.current
  140. end
  141. end

app/models/user.rb

75.89% lines covered

112 relevant lines. 85 lines covered and 27 lines missed.
    
  1. 1 require 'jwt'
  2. 1 class User < ApplicationRecord
  3. # 关联
  4. 1 has_many :created_events, class_name: "ReadingEvent", foreign_key: "leader_id", dependent: :destroy
  5. 1 has_many :enrollments, class_name: "EventEnrollment", dependent: :destroy
  6. 1 has_many :event_enrollments, class_name: "EventEnrollment", dependent: :destroy # 为了兼容分析系统
  7. 1 has_many :reading_events, through: :enrollments
  8. 1 has_many :posts, dependent: :destroy
  9. 1 has_many :check_ins, dependent: :destroy
  10. 1 has_many :comments, dependent: :destroy
  11. # 小红花相关关联
  12. 1 has_many :received_flowers, class_name: "Flower", foreign_key: "recipient_id", dependent: :destroy
  13. 1 has_many :given_flowers, class_name: "Flower", foreign_key: "giver_id", dependent: :destroy
  14. 1 has_many :flowers, foreign_key: "giver_id", dependent: :destroy # 为了兼容分析系统
  15. 1 has_many :flower_quotas, dependent: :destroy
  16. 1 has_many :flower_certificates, dependent: :destroy
  17. # 通知相关关联
  18. 1 has_many :received_notifications, class_name: "Notification", foreign_key: "recipient_id", dependent: :destroy
  19. 1 has_many :sent_notifications, class_name: "Notification", foreign_key: "actor_id", dependent: :destroy
  20. # 验证
  21. 1 validates :wx_openid, presence: true, uniqueness: true
  22. 1 validates :wx_unionid, uniqueness: true, allow_nil: true
  23. 1 validates :nickname, presence: true, length: { minimum: 1, maximum: 50 }, allow_blank: false
  24. # 枚举:用户角色(暂时注释掉以解决API问题)
  25. # enum role: %w[user admin root], default: 'user'
  26. # 生成 JWT token
  27. 1 def generate_jwt_token
  28. payload = {
  29. 2 user_id: id,
  30. wx_openid: wx_openid,
  31. role: role_as_string, # 使用字符串角色名
  32. exp: 30.days.from_now.to_i,
  33. iat: Time.current.to_i, # 签发时间
  34. type: 'access' # token类型
  35. }
  36. 2 JWT.encode(payload, Rails.application.credentials.jwt_secret_key || "dev_secret_key")
  37. end
  38. # 生成refresh token(长期有效)
  39. 1 def generate_refresh_token
  40. payload = {
  41. user_id: id,
  42. wx_openid: wx_openid,
  43. type: 'refresh',
  44. exp: 90.days.from_now.to_i, # 90天有效期
  45. iat: Time.current.to_i
  46. }
  47. JWT.encode(payload, Rails.application.credentials.jwt_secret_key || "dev_secret_key")
  48. end
  49. # 解析refresh token
  50. 1 def self.decode_refresh_token(token)
  51. begin
  52. decoded = JWT.decode(token, Rails.application.credentials.jwt_secret_key || "dev_secret_key")[0]
  53. return nil unless decoded['type'] == 'refresh'
  54. HashWithIndifferentAccess.new(decoded)
  55. rescue JWT::DecodeError => e
  56. Rails.logger.warn "Refresh token解码失败: #{e.message}"
  57. nil
  58. end
  59. end
  60. # 使用refresh token生成新的access token
  61. 1 def self.refresh_access_token(refresh_token)
  62. decoded = decode_refresh_token(refresh_token)
  63. return nil unless decoded
  64. user = User.find_by(id: decoded['user_id'])
  65. return nil unless user
  66. # 验证openid是否匹配
  67. return nil unless user.wx_openid == decoded['wx_openid']
  68. # 生成新的access token
  69. new_access_token = user.generate_jwt_token
  70. {
  71. access_token: new_access_token,
  72. refresh_token: refresh_token, # refresh token可以继续使用
  73. user: user.as_json_for_api
  74. }
  75. end
  76. # 解析 JWT token
  77. 1 def self.decode_jwt_token(token)
  78. begin
  79. # 移除 Bearer 前缀
  80. 3 token = token.gsub('Bearer ', '') if token&.start_with?('Bearer ')
  81. 3 Rails.logger.info "JWT Token: #{token[0..50]}..." if token
  82. 3 secret = Rails.application.credentials.jwt_secret_key || "dev_secret_key"
  83. 3 Rails.logger.info "JWT Secret: #{secret[0..10]}..." if secret
  84. 3 decoded = JWT.decode(token, secret)[0]
  85. 1 Rails.logger.info "JWT Decoded: #{decoded.inspect}"
  86. 1 HashWithIndifferentAccess.new(decoded)
  87. rescue JWT::DecodeError => e
  88. 2 Rails.logger.error "JWT Decode Error: #{e.message}"
  89. 2 nil
  90. rescue => e
  91. Rails.logger.error "JWT Unexpected Error: #{e.message}"
  92. nil
  93. end
  94. end
  95. # 简化的角色权限检查方法
  96. 1 def user?
  97. 5 role.to_s == 'user' || role.to_s == '0'
  98. end
  99. 1 def participant?
  100. role.to_s == 'user' || role.to_s == '0' # 同义词,与user相同
  101. end
  102. 1 def admin?
  103. 34 role.to_s == 'admin' || role.to_s == '1'
  104. end
  105. 1 def root?
  106. 33 role.to_s == 'root' || role.to_s == '2'
  107. end
  108. 1 def any_admin?
  109. 15 admin? || root?
  110. end
  111. # 管理员权限检查
  112. 1 def can_manage_users?
  113. 6 root?
  114. end
  115. 1 def can_approve_events?
  116. 6 admin? || root?
  117. end
  118. 1 def can_view_approval_queue?
  119. admin? || root?
  120. end
  121. 1 def can_view_admin_panel?
  122. 6 admin? || root?
  123. end
  124. 1 def can_manage_system?
  125. 6 root?
  126. end
  127. # 基础用户权限
  128. 1 def can_create_posts?
  129. 5 true # 所有用户都可以发帖
  130. end
  131. 1 def can_comment?
  132. 4 true # 所有用户都可以评论
  133. end
  134. 1 def can_join_events?
  135. 4 true # 所有用户都可以报名活动
  136. end
  137. # 活动相关权限检查(基于 Enrollment,不是角色)
  138. 1 def is_event_leader?(event)
  139. 3 return false unless event
  140. 2 event.leader_id == id
  141. end
  142. 1 def is_daily_leader?(event, schedule)
  143. return false unless event && schedule
  144. return false unless schedule.reading_event_id == event.id
  145. schedule.daily_leader_id == id
  146. end
  147. # 角色提升方法
  148. 1 def promote_to_admin!
  149. update!(role: 1) if user? || participant? # 1 represents admin in integer form
  150. end
  151. 1 def demote_to_user!
  152. 1 update!(role: 0) # 0 represents user in integer form
  153. end
  154. # 获取角色显示名称
  155. 1 def role_display_name
  156. 9 case role.to_s
  157. when 'user', '0'
  158. 6 '用户'
  159. when 'admin', '1'
  160. 1 '管理员'
  161. when 'root', '2'
  162. 1 '超级管理员'
  163. else
  164. 1 '未知角色'
  165. end
  166. end
  167. # 获取角色字符串名称(用于JWT token)
  168. 1 def role_as_string
  169. 3 case role.to_s
  170. when 'user', '0'
  171. 3 'user'
  172. when 'admin', '1'
  173. 'admin'
  174. when 'root', '2'
  175. 'root'
  176. else
  177. 'user' # 默认为user
  178. end
  179. end
  180. # 检查用户是否有特定权限
  181. 1 def has_permission?(permission)
  182. 16 case permission
  183. when :approve_events
  184. 3 can_approve_events?
  185. when :manage_users
  186. 3 can_manage_users?
  187. when :view_admin_panel
  188. 3 can_view_admin_panel?
  189. when :manage_system
  190. 3 can_manage_system?
  191. when :create_posts
  192. 2 can_create_posts?
  193. when :comment
  194. 1 can_comment?
  195. when :join_events
  196. 1 can_join_events?
  197. else
  198. false
  199. end
  200. end
  201. # 用于API响应的用户信息格式化
  202. 1 def as_json_for_api
  203. {
  204. id: id,
  205. nickname: nickname,
  206. wx_openid: wx_openid,
  207. avatar_url: avatar_url,
  208. phone: phone,
  209. role: role_as_string
  210. }
  211. end
  212. end

app/models/user_activity.rb

0.0% lines covered

245 relevant lines. 0 lines covered and 245 lines missed.
    
  1. # frozen_string_literal: true
  2. # UserActivity - 用户活动模型
  3. # 记录用户的各种行为和活动轨迹
  4. class UserActivity < ApplicationRecord
  5. belongs_to :user
  6. validates :user, presence: true
  7. validates :action_type, presence: true
  8. validates :details, presence: true
  9. # 活动类型枚举
  10. enum :action_type, {
  11. # 内容相关
  12. post_created: 'post_created',
  13. post_updated: 'post_updated',
  14. post_deleted: 'post_deleted',
  15. comment_created: 'comment_created',
  16. comment_updated: 'comment_updated',
  17. comment_deleted: 'comment_deleted',
  18. like_given: 'like_given',
  19. like_removed: 'like_removed',
  20. # 活动相关
  21. event_joined: 'event_joined',
  22. event_left: 'event_left',
  23. event_completed: 'event_completed',
  24. check_in_created: 'check_in_created',
  25. flower_given: 'flower_given',
  26. flower_received: 'flower_received',
  27. # 社交相关
  28. profile_viewed: 'profile_viewed',
  29. user_followed: 'user_followed',
  30. user_unfollowed: 'user_unfollowed',
  31. # 系统相关
  32. login: 'login',
  33. logout: 'logout',
  34. password_changed: 'password_changed',
  35. profile_updated: 'profile_updated',
  36. settings_changed: 'settings_changed',
  37. # 页面浏览
  38. page_view: 'page_view',
  39. api_call: 'api_call'
  40. }
  41. # 作用域
  42. scope :recent, -> { order(created_at: :desc) }
  43. scope :today, -> { where(created_at: Date.current.all_day) }
  44. scope :this_week, -> { where(created_at: Date.current.beginning_of_week..Date.current.end_of_week) }
  45. scope :this_month, -> { where(created_at: Date.current.beginning_of_month..Date.current.end_of_month) }
  46. scope :by_action_type, ->(type) { where(action_type: type) }
  47. scope :by_user, ->(user) { where(user: user) }
  48. # 类方法:记录用户活动
  49. def self.track(user:, action_type:, details: {})
  50. return unless user
  51. create!(
  52. user: user,
  53. action_type: action_type,
  54. details: details.merge(
  55. timestamp: Time.current.iso8601,
  56. ip: details[:ip] || '0.0.0.0',
  57. user_agent: details[:user_agent] || 'Unknown'
  58. )
  59. )
  60. rescue => e
  61. Rails.logger.error "Failed to track user activity: #{e.message}"
  62. end
  63. # 类方法:获取用户活动统计
  64. def self.activity_stats(user, period = :week)
  65. case period
  66. when :day
  67. start_time = Date.current.beginning_of_day
  68. end_time = Date.current.end_of_day
  69. when :week
  70. start_time = Date.current.beginning_of_week
  71. end_time = Date.current.end_of_week
  72. when :month
  73. start_time = Date.current.beginning_of_month
  74. end_time = Date.current.end_of_month
  75. else
  76. start_time = 30.days.ago
  77. end_time = Time.current
  78. end
  79. activities = where(user: user)
  80. .where(created_at: start_time..end_time)
  81. {
  82. total_activities: activities.count,
  83. action_breakdown: activities.group(:action_type).count,
  84. most_active_day: find_most_active_day(activities),
  85. average_daily_activities: calculate_daily_average(activities, period)
  86. }
  87. end
  88. # 类方法:获取用户最近活动
  89. def self.recent_activities(user, limit = 10)
  90. where(user: user)
  91. .recent
  92. .limit(limit)
  93. end
  94. # 类方法:清理旧活动记录
  95. def self.cleanup_old_activities(days_to_keep = 90)
  96. cutoff_date = days_to_keep.days.ago
  97. where('created_at < ?', cutoff_date).delete_all
  98. end
  99. # 类方法:获取活动趋势
  100. def self.activity_trend(user, days = 7)
  101. end_date = Date.current
  102. start_date = days.days.ago.to_date
  103. activities = where(user: user)
  104. .where(created_at: start_date.beginning_of_day..end_date.end_of_day)
  105. .group_by_day(:created_at)
  106. .count
  107. trend_data = []
  108. (start_date..end_date).each do |date|
  109. trend_data << {
  110. date: date.iso8601,
  111. count: activities[date] || 0
  112. }
  113. end
  114. trend_data
  115. end
  116. # 类方法:获取用户活跃度评分
  117. def self.activity_score(user)
  118. # 基于最近30天的活动计算活跃度评分
  119. cutoff_date = 30.days.ago
  120. recent_activities = where(user: user)
  121. .where('created_at > ?', cutoff_date)
  122. score = 0
  123. # 内容创作得分
  124. content_actions = %w[post_created comment_created]
  125. content_score = recent_activities.where(action_type: content_actions).count * 10
  126. score += content_score
  127. # 社交互动得分
  128. social_actions = %w[like_given flower_given event_joined]
  129. social_score = recent_activities.where(action_type: social_actions).count * 5
  130. score += social_score
  131. # 登录活跃度得分
  132. login_actions = %w[login page_view api_call]
  133. login_score = [recent_activities.where(action_type: login_actions).count, 100].min
  134. score += login_score
  135. # 时间衰减因子(越近的活动权重越高)
  136. time_decay_factor = calculate_time_decay_factor(recent_activities)
  137. score = (score * time_decay_factor).round
  138. {
  139. score: score,
  140. level: activity_level(score),
  141. content_score: content_score,
  142. social_score: social_score,
  143. login_score: login_score,
  144. time_decay_factor: time_decay_factor
  145. }
  146. end
  147. # 实例方法:格式化活动描述
  148. def formatted_description
  149. case action_type
  150. when 'post_created'
  151. "发布了新帖子「#{details['post_title']}」"
  152. when 'comment_created'
  153. "评论了帖子「#{details['post_title']}」"
  154. when 'like_given'
  155. "点赞了#{details['target_type']}「#{details['target_title']}」"
  156. when 'event_joined'
  157. "参加了活动「#{details['event_title']}」"
  158. when 'flower_given'
  159. "给#{details['recipient_name']}送了一朵小红花"
  160. when 'login'
  161. "登录了系统"
  162. when 'page_view'
  163. "浏览了#{details['path']}页面"
  164. else
  165. action_type.humanize
  166. end
  167. end
  168. # 实例方法:获取活动图标
  169. def icon
  170. case action_type
  171. when 'post_created', 'post_updated'
  172. 'edit'
  173. when 'comment_created', 'comment_updated'
  174. 'comment'
  175. when 'like_given'
  176. 'heart'
  177. when 'event_joined', 'event_completed'
  178. 'calendar'
  179. when 'flower_given'
  180. 'flower'
  181. when 'login'
  182. 'log-in'
  183. when 'page_view'
  184. 'eye'
  185. else
  186. 'activity'
  187. end
  188. end
  189. # 实例方法:获取活动颜色
  190. def color
  191. case action_type
  192. when 'post_created', 'comment_created', 'like_given'
  193. 'blue'
  194. when 'event_joined', 'event_completed'
  195. 'green'
  196. when 'flower_given'
  197. 'red'
  198. when 'login'
  199. 'gray'
  200. else
  201. 'default'
  202. end
  203. end
  204. # 实例方法:是否为重要活动
  205. def important?
  206. %w[post_created event_joined flower_given].include?(action_type)
  207. end
  208. # 实例方法:获取活动链接
  209. def activity_link
  210. case action_type
  211. when 'post_created', 'post_updated'
  212. "/posts/#{details['post_id']}" if details['post_id']
  213. when 'comment_created'
  214. "/posts/#{details['post_id']}#comment-#{details['comment_id']}" if details['post_id'] && details['comment_id']
  215. when 'event_joined', 'event_completed'
  216. "/events/#{details['event_id']}" if details['event_id']
  217. when 'profile_viewed'
  218. "/users/#{details['profile_user_id']}" if details['profile_user_id']
  219. else
  220. nil
  221. end
  222. end
  223. private
  224. def self.find_most_active_day(activities)
  225. day_counts = activities.group_by_day(:created_at).count
  226. return nil if day_counts.empty?
  227. most_active_date = day_counts.max_by { |_, count| count }&.first
  228. return nil unless most_active_date
  229. {
  230. date: most_active_date.iso8601,
  231. count: day_counts[most_active_date]
  232. }
  233. end
  234. def self.calculate_daily_average(activities, period)
  235. case period
  236. when :day
  237. activities.count.to_f
  238. when :week
  239. (activities.count / 7.0).round(2)
  240. when :month
  241. (activities.count / 30.0).round(2)
  242. else
  243. (activities.count / 7.0).round(2)
  244. end
  245. end
  246. def self.calculate_time_decay_factor(activities)
  247. return 0.0 if activities.empty?
  248. # 计算时间衰减因子,越近的活动权重越高
  249. total_weight = 0.0
  250. total_activities = activities.count
  251. activities.each do |activity|
  252. days_ago = (Time.current - activity.created_at) / 1.day
  253. weight = Math.exp(-days_ago / 7.0) # 7天衰减常数
  254. total_weight += weight
  255. end
  256. (total_weight / total_activities).round(3)
  257. end
  258. def self.activity_level(score)
  259. case score
  260. when 0..10
  261. 'inactive'
  262. when 11..50
  263. 'low'
  264. when 51..150
  265. 'moderate'
  266. when 151..300
  267. 'high'
  268. else
  269. 'very_high'
  270. end
  271. end
  272. end

app/services/activity_approval_workflow_service.rb

0.0% lines covered

539 relevant lines. 0 lines covered and 539 lines missed.
    
  1. # frozen_string_literal: true
  2. # ActivityApprovalWorkflowService - 活动审批工作流服务
  3. # 负责活动审批的完整业务流程,包括多级审批、条件检查、通知等
  4. class ActivityApprovalWorkflowService < ApplicationService
  5. attr_reader :event, :admin_user, :action, :approval_options, :workflow_type
  6. def initialize(event:, admin_user:, action:, workflow_type: :standard, approval_options: {})
  7. super()
  8. @event = event
  9. @admin_user = admin_user
  10. @action = action
  11. @workflow_type = workflow_type
  12. @approval_options = approval_options ? approval_options.with_indifferent_access : {}.with_indifferent_access
  13. end
  14. def call
  15. handle_errors do
  16. case action
  17. when :submit_for_approval
  18. submit_for_approval
  19. when :approve
  20. process_approval
  21. when :reject
  22. process_rejection
  23. when :batch_approve
  24. batch_approve_events
  25. when :batch_reject
  26. batch_reject_events
  27. when :get_approval_queue
  28. get_approval_queue
  29. when :get_approval_statistics
  30. get_approval_statistics
  31. when :escalate
  32. escalate_approval
  33. else
  34. failure!("不支持的审批操作: #{action}")
  35. end
  36. end
  37. end
  38. # 类方法:提交审批
  39. def self.submit_for_approval!(event, workflow_type: :standard)
  40. service = new(event: event, admin_user: event.leader, action: :submit_for_approval, workflow_type: workflow_type)
  41. service.call
  42. service
  43. end
  44. # 类方法:审批通过
  45. def self.approve!(event, admin_user, reason: nil, notes: nil)
  46. service = new(event: event, admin_user: admin_user, action: :approve,
  47. approval_options: { reason: reason, notes: notes })
  48. service.call
  49. service
  50. end
  51. # 类方法:审批拒绝
  52. def self.reject!(event, admin_user, reason, notes: nil)
  53. service = new(event: event, admin_user: admin_user, action: :reject,
  54. approval_options: { reason: reason, notes: notes })
  55. service.call
  56. service
  57. end
  58. # 类方法:批量审批
  59. def self.batch_approve!(event_ids, admin_user, reason: nil)
  60. service = new(event: nil, admin_user: admin_user, action: :batch_approve,
  61. approval_options: { event_ids: event_ids, reason: reason })
  62. service.call
  63. service
  64. end
  65. # 类方法:批量拒绝
  66. def self.batch_reject!(event_ids, admin_user, reason)
  67. service = new(event: nil, admin_user: admin_user, action: :batch_reject,
  68. approval_options: { event_ids: event_ids, reason: reason })
  69. service.call
  70. service
  71. end
  72. # 类方法:获取审批队列
  73. def self.approval_queue(admin_user, filters = {})
  74. service = new(event: nil, admin_user: admin_user, action: :get_approval_queue,
  75. approval_options: filters)
  76. service.call
  77. service
  78. end
  79. # 类方法:获取审批统计
  80. def self.approval_statistics(admin_user, date_range: nil)
  81. service = new(event: nil, admin_user: admin_user, action: :get_approval_statistics,
  82. approval_options: { date_range: date_range })
  83. service.call
  84. service
  85. end
  86. # 类方法:升级审批
  87. def self.escalate!(event, admin_user, escalation_reason)
  88. service = new(event: event, admin_user: admin_user, action: :escalate,
  89. approval_options: { escalation_reason: escalation_reason })
  90. service.call
  91. service
  92. end
  93. private
  94. # 提交审批申请
  95. def submit_for_approval
  96. # 检查是否可以提交审批
  97. unless event.can_submit_for_approval?
  98. return failure!("活动当前状态无法提交审批")
  99. end
  100. # 检查审批前置条件
  101. validation_result = validate_event_for_approval
  102. unless validation_result[:valid]
  103. return failure!(validation_result[:errors].join(", "))
  104. end
  105. ActiveRecord::Base.transaction do
  106. # 更新活动状态为待审批
  107. event.update!(
  108. status: :draft,
  109. approval_status: :pending,
  110. submitted_for_approval_at: Time.current
  111. )
  112. # 记录审批日志
  113. create_approval_log(:submitted, event.leader, "提交审批申请")
  114. # 发送通知给审批管理员(暂时注释,等待通知系统实现)
  115. # send_approval_notifications
  116. success!({
  117. message: "活动已提交审批,请等待管理员审核",
  118. event: event_approval_info,
  119. approval_queue_position: get_approval_queue_position
  120. })
  121. end
  122. rescue => e
  123. failure!("提交审批失败: #{e.message}")
  124. end
  125. # 处理审批通过
  126. def process_approval
  127. # 检查审批权限
  128. unless admin_user.can_approve_events?
  129. return failure!("权限不足,无法审批活动")
  130. end
  131. # 检查活动状态
  132. unless event.pending_approval?
  133. return failure!("活动当前状态无法审批")
  134. end
  135. # 执行审批通过流程
  136. ActiveRecord::Base.transaction do
  137. # 更新活动状态
  138. event.update!(
  139. status: :enrolling,
  140. approval_status: :approved,
  141. approved_by_id: admin_user.id,
  142. approved_at: Time.current,
  143. approval_reason: @approval_options[:reason],
  144. approval_notes: @approval_options[:notes]
  145. )
  146. # 记录审批日志
  147. create_approval_log(:approved, admin_user, @approval_options[:reason])
  148. # 发送审批通过通知
  149. send_approval_decision_notification(:approved)
  150. success!({
  151. message: "活动审批通过",
  152. event: event_approval_info,
  153. approval_details: approval_decision_info
  154. })
  155. end
  156. rescue => e
  157. failure!("审批通过失败: #{e.message}")
  158. end
  159. # 处理审批拒绝
  160. def process_rejection
  161. # 检查审批权限
  162. unless admin_user.can_approve_events?
  163. return failure!("权限不足,无法审批活动")
  164. end
  165. # 检查拒绝理由
  166. rejection_reason = @approval_options[:reason]
  167. if rejection_reason.blank?
  168. return failure!("请提供拒绝理由")
  169. end
  170. # 检查活动状态
  171. unless event.pending_approval?
  172. return failure!("活动当前状态无法审批")
  173. end
  174. ActiveRecord::Base.transaction do
  175. # 更新活动状态
  176. event.update!(
  177. approval_status: :rejected,
  178. approved_by_id: admin_user.id,
  179. approved_at: Time.current,
  180. rejection_reason: rejection_reason,
  181. approval_notes: @approval_options[:notes]
  182. )
  183. # 记录审批日志
  184. create_approval_log(:rejected, admin_user, rejection_reason)
  185. # 发送审批拒绝通知
  186. send_approval_decision_notification(:rejected)
  187. success!({
  188. message: "活动已拒绝",
  189. event: event_approval_info,
  190. rejection_details: {
  191. reason: rejection_reason,
  192. notes: @approval_options[:notes],
  193. resubmission_allowed: event.can_resubmit_for_approval?
  194. }
  195. })
  196. end
  197. rescue => e
  198. failure!("审批拒绝失败: #{e.message}")
  199. end
  200. # 批量审批通过
  201. def batch_approve_events
  202. unless admin_user.can_approve_events?
  203. return failure!("权限不足,无法批量审批活动")
  204. end
  205. event_ids = @approval_options[:event_ids]
  206. if event_ids.blank? || !event_ids.is_a?(Array)
  207. return failure!("请提供有效的活动ID列表")
  208. end
  209. events = ReadingEvent.where(id: event_ids, approval_status: :pending)
  210. if events.empty?
  211. return failure!("没有找到待审批的活动")
  212. end
  213. approval_results = []
  214. failed_count = 0
  215. ActiveRecord::Base.transaction do
  216. events.each do |event_item|
  217. begin
  218. event_item.update!(
  219. status: :enrolling,
  220. approval_status: :approved,
  221. approved_by_id: admin_user.id,
  222. approved_at: Time.current,
  223. approval_reason: @approval_options[:reason]
  224. )
  225. create_approval_log(:approved, admin_user, @approval_options[:reason], event_item)
  226. approval_results << { event_id: event_item.id, status: 'approved', success: true }
  227. rescue => e
  228. failed_count += 1
  229. approval_results << { event_id: event_item.id, status: 'failed', error: e.message, success: false }
  230. end
  231. end
  232. end
  233. success!({
  234. message: "批量审批完成,成功: #{events.count - failed_count},失败: #{failed_count}",
  235. batch_results: approval_results,
  236. summary: {
  237. total: events.count,
  238. successful: events.count - failed_count,
  239. failed: failed_count
  240. }
  241. })
  242. rescue => e
  243. failure!("批量审批失败: #{e.message}")
  244. end
  245. # 批量审批拒绝
  246. def batch_reject_events
  247. unless admin_user.can_approve_events?
  248. return failure!("权限不足,无法批量审批活动")
  249. end
  250. rejection_reason = @approval_options[:reason]
  251. if rejection_reason.blank?
  252. return failure!("请提供拒绝理由")
  253. end
  254. event_ids = @approval_options[:event_ids]
  255. if event_ids.blank? || !event_ids.is_a?(Array)
  256. return failure!("请提供有效的活动ID列表")
  257. end
  258. events = ReadingEvent.where(id: event_ids, approval_status: :pending)
  259. if events.empty?
  260. return failure!("没有找到待审批的活动")
  261. end
  262. rejection_results = []
  263. failed_count = 0
  264. ActiveRecord::Base.transaction do
  265. events.each do |event_item|
  266. begin
  267. event_item.update!(
  268. approval_status: :rejected,
  269. approved_by_id: admin_user.id,
  270. approved_at: Time.current,
  271. rejection_reason: rejection_reason,
  272. approval_notes: @approval_options[:notes]
  273. )
  274. create_approval_log(:rejected, admin_user, rejection_reason, event_item)
  275. rejection_results << { event_id: event_item.id, status: 'rejected', success: true }
  276. rescue => e
  277. failed_count += 1
  278. rejection_results << { event_id: event_item.id, status: 'failed', error: e.message, success: false }
  279. end
  280. end
  281. end
  282. success!({
  283. message: "批量拒绝完成,成功: #{events.count - failed_count},失败: #{failed_count}",
  284. batch_results: rejection_results,
  285. summary: {
  286. total: events.count,
  287. successful: events.count - failed_count,
  288. failed: failed_count
  289. }
  290. })
  291. rescue => e
  292. failure!("批量拒绝失败: #{e.message}")
  293. end
  294. # 获取审批队列
  295. def get_approval_queue
  296. # 检查查看权限
  297. unless admin_user.can_approve_events? || admin_user.can_view_approval_queue?
  298. return failure!("权限不足,无法查看审批队列")
  299. end
  300. filters = @approval_options
  301. events = ReadingEvent.includes(:leader).where(approval_status: :pending)
  302. # 应用过滤条件
  303. events = apply_approval_queue_filters(events, filters)
  304. # 排序
  305. events = events.order(submitted_for_approval_at: :asc)
  306. # 分页
  307. page = filters[:page] || 1
  308. per_page = filters[:per_page] || 20
  309. total_count = events.count
  310. paginated_events = events.limit(per_page).offset((page - 1) * per_page)
  311. queue_data = paginated_events.map do |event_item|
  312. event_approval_queue_info(event_item)
  313. end
  314. success!({
  315. approval_queue: queue_data,
  316. pagination: {
  317. current_page: page,
  318. per_page: per_page,
  319. total_count: total_count,
  320. total_pages: (total_count.to_f / per_page).ceil
  321. },
  322. filters_applied: filters,
  323. queue_statistics: get_queue_statistics(events)
  324. })
  325. end
  326. # 获取审批统计
  327. def get_approval_statistics
  328. unless admin_user.can_approve_events?
  329. return failure!("权限不足,无法查看审批统计")
  330. end
  331. date_range = @approval_options[:date_range] || (Date.today - 30.days)..Date.today
  332. stats = {
  333. total_pending: ReadingEvent.where(approval_status: :pending).count,
  334. total_approved: ReadingEvent.where(approval_status: :approved).count,
  335. total_rejected: ReadingEvent.where(approval_status: :rejected).count,
  336. # 期间统计
  337. period_approved: ReadingEvent.where(approval_status: :approved, approved_at: date_range).count,
  338. period_rejected: ReadingEvent.where(approval_status: :rejected, approved_at: date_range).count,
  339. # 审批效率统计
  340. average_approval_time: calculate_average_approval_time(date_range),
  341. approval_rate: calculate_approval_rate(date_range),
  342. # 管理员统计
  343. admin_stats: get_admin_approval_stats(date_range),
  344. # 活动类型统计
  345. activity_mode_stats: get_activity_mode_approval_stats(date_range)
  346. }
  347. success!(stats)
  348. end
  349. # 升级审批
  350. def escalate_approval
  351. unless event.pending_approval?
  352. return failure!("只有待审批的活动可以升级审批")
  353. end
  354. escalation_reason = @approval_options[:escalation_reason]
  355. if escalation_reason.blank?
  356. return failure!("请提供升级理由")
  357. end
  358. ActiveRecord::Base.transaction do
  359. # 记录升级日志
  360. create_approval_log(:escalated, admin_user, escalation_reason)
  361. # 发送升级通知给高级管理员
  362. send_escalation_notification(escalation_reason)
  363. success!({
  364. message: "审批已升级给高级管理员",
  365. event: event_approval_info,
  366. escalation_details: {
  367. reason: escalation_reason,
  368. escalated_by: admin_user_info,
  369. escalated_at: Time.current
  370. }
  371. })
  372. end
  373. rescue => e
  374. failure!("升级审批失败: #{e.message}")
  375. end
  376. # 辅助方法
  377. # 验证活动是否满足审批条件
  378. def validate_event_for_approval
  379. errors = []
  380. # 检查基本信息
  381. errors << "活动标题不能为空" if event.title.blank?
  382. errors << "活动描述不能为空" if event.description.blank?
  383. errors << "书籍名称不能为空" if event.book_name.blank?
  384. # 检查日期设置
  385. errors << "开始日期不能为空" if event.start_date.blank?
  386. errors << "结束日期不能为空" if event.end_date.blank?
  387. errors << "开始日期必须在今天之后" if event.start_date <= Date.today
  388. # 检查人数设置
  389. errors << "最大参与人数必须大于0" if event.max_participants.nil? || event.max_participants <= 0
  390. errors << "最小参与人数不能大于最大参与人数" if event.min_participants > event.max_participants
  391. # 检查费用设置(如果是收费活动)
  392. if event.fee_type != 'free'
  393. errors << "收费活动必须设置费用金额" if event.fee_amount.nil? || event.fee_amount <= 0
  394. errors << "收费活动必须设置领读人奖励比例" if event.leader_reward_percentage.nil?
  395. end
  396. # 检查阅读计划
  397. if event.reading_schedules.empty?
  398. errors << "必须设置阅读计划"
  399. end
  400. # 检查特定活动模式的特殊要求
  401. case event.activity_mode
  402. when 'video_conference'
  403. errors << "视频会议活动必须设置会议链接" if event.meeting_link.blank?
  404. when 'offline_meeting'
  405. errors << "线下活动必须设置活动地点" if event.location.blank?
  406. end
  407. {
  408. valid: errors.empty?,
  409. errors: errors
  410. }
  411. end
  412. # 应用审批队列过滤条件
  413. def apply_approval_queue_filters(events, filters)
  414. events = events.where(leader_id: filters[:leader_id]) if filters[:leader_id].present?
  415. events = events.where(activity_mode: filters[:activity_mode]) if filters[:activity_mode].present?
  416. events = events.where(fee_type: filters[:fee_type]) if filters[:fee_type].present?
  417. events = events.where('submitted_for_approval_at >= ?', filters[:submitted_since]) if filters[:submitted_since].present?
  418. events = events.where('submitted_for_approval_at <= ?', filters[:submitted_until]) if filters[:submitted_until].present?
  419. events
  420. end
  421. # 计算平均审批时间
  422. def calculate_average_approval_time(date_range)
  423. approved_events = ReadingEvent.where(
  424. approval_status: :approved,
  425. approved_at: date_range
  426. ).where.not(submitted_for_approval_at: nil)
  427. return 0 if approved_events.empty?
  428. total_time = approved_events.sum do |event|
  429. (event.approved_at - event.submitted_for_approval_at) / 1.hour
  430. end
  431. (total_time / approved_events.count).round(2)
  432. end
  433. # 计算审批通过率
  434. def calculate_approval_rate(date_range)
  435. total_events = ReadingEvent.where(
  436. approved_at: date_range
  437. ).where.not(approval_status: :pending)
  438. return 0 if total_events.empty?
  439. approved_count = total_events.where(approval_status: :approved).count
  440. (approved_count.to_f / total_events.count * 100).round(2)
  441. end
  442. # 获取管理员审批统计
  443. def get_admin_approval_stats(date_range)
  444. approved_events = ReadingEvent.includes(:approver)
  445. .where(approval_status: :approved, approved_at: date_range)
  446. .where.not(approved_by_id: nil)
  447. stats = {}
  448. approved_events.each do |event|
  449. admin_id = event.approved_by_id
  450. stats[admin_id] ||= {
  451. name: event.approver&.nickname || 'Unknown',
  452. approved_count: 0,
  453. rejected_count: 0
  454. }
  455. stats[admin_id][:approved_count] += 1
  456. end
  457. rejected_events = ReadingEvent.includes(:approver)
  458. .where(approval_status: :rejected, approved_at: date_range)
  459. .where.not(approved_by_id: nil)
  460. rejected_events.each do |event|
  461. admin_id = event.approved_by_id
  462. stats[admin_id] ||= {
  463. name: event.approver&.nickname || 'Unknown',
  464. approved_count: 0,
  465. rejected_count: 0
  466. }
  467. stats[admin_id][:rejected_count] += 1
  468. end
  469. stats.values
  470. end
  471. # 获取活动模式审批统计
  472. def get_activity_mode_approval_stats(date_range)
  473. modes = %w[note_checkin free_discussion video_conference offline_meeting]
  474. stats = {}
  475. modes.each do |mode|
  476. total = ReadingEvent.where(
  477. activity_mode: mode,
  478. approved_at: date_range
  479. ).where.not(approval_status: :pending).count
  480. approved = ReadingEvent.where(
  481. activity_mode: mode,
  482. approval_status: :approved,
  483. approved_at: date_range
  484. ).count
  485. stats[mode] = {
  486. total: total,
  487. approved: approved,
  488. rejected: total - approved,
  489. approval_rate: total > 0 ? (approved.to_f / total * 100).round(2) : 0
  490. }
  491. end
  492. stats
  493. end
  494. # 获取队列统计信息
  495. def get_queue_statistics(events)
  496. {
  497. total_pending: events.count,
  498. pending_by_fee_type: events.group(:fee_type).count,
  499. pending_by_activity_mode: events.group(:activity_mode).count,
  500. oldest_pending_age: events.maximum(:submitted_for_approval_at) ?
  501. ((Time.current - events.maximum(:submitted_for_approval_at)) / 1.day).round(1) : 0,
  502. average_pending_age: events.average(:submitted_for_approval_at) ?
  503. ((Time.current - events.average(:submitted_for_approval_at)) / 1.day).round(1) : 0
  504. }
  505. end
  506. # 创建审批日志
  507. def create_approval_log(action, operator, reason, target_event = event)
  508. # 这里应该创建一个 ApprovalLog 模型来记录审批历史
  509. # 暂时使用 Rails logger 记录
  510. Rails.logger.info "审批日志: #{action} - 活动 #{target_event.id} - 操作者 #{operator.nickname} - 理由: #{reason}"
  511. end
  512. # 发送审批决定通知
  513. def send_approval_decision_notification(decision)
  514. # 这里应该实现通知系统
  515. Rails.logger.info "审批通知: 活动 #{event.id} 已被#{decision == :approved ? '通过' : '拒绝'}"
  516. end
  517. # 发送升级通知
  518. def send_escalation_notification(reason)
  519. # 这里应该实现升级通知给高级管理员
  520. Rails.logger.info "审批升级: 活动 #{event.id} 需要高级管理员审批 - 理由: #{reason}"
  521. end
  522. # 获取审批队列位置
  523. def get_approval_queue_position
  524. ReadingEvent.where(approval_status: :pending)
  525. .where('submitted_for_approval_at <= ?', event.submitted_for_approval_at)
  526. .count
  527. end
  528. # 格式化活动审批信息
  529. def event_approval_info
  530. {
  531. id: event.id,
  532. title: event.title,
  533. book_name: event.book_name,
  534. activity_mode: event.activity_mode,
  535. fee_type: event.fee_type,
  536. fee_amount: event.fee_amount,
  537. max_participants: event.max_participants,
  538. start_date: event.start_date,
  539. end_date: event.end_date,
  540. leader: user_info(event.leader),
  541. status: event.status,
  542. approval_status: event.approval_status,
  543. submitted_for_approval_at: event.submitted_for_approval_at,
  544. approved_at: event.approved_at,
  545. approver: event.approver ? user_info(event.approver) : nil
  546. }
  547. end
  548. # 格式化活动审批队列信息
  549. def event_approval_queue_info(event_item)
  550. {
  551. id: event_item.id,
  552. title: event_item.title,
  553. book_name: event_item.book_name,
  554. activity_mode: event_item.activity_mode,
  555. fee_type: event_item.fee_type,
  556. fee_amount: event_item.fee_amount,
  557. max_participants: event_item.max_participants,
  558. start_date: event_item.start_date,
  559. end_date: event_item.end_date,
  560. leader: user_info(event_item.leader),
  561. submitted_for_approval_at: event_item.submitted_for_approval_at,
  562. pending_age_days: event_item.submitted_for_approval_at ?
  563. ((Time.current - event_item.submitted_for_approval_at) / 1.day).round(1) : 0,
  564. validation_status: event_item.validate_event_for_approval[:valid] ? 'valid' : 'invalid',
  565. requires_attention: requires_immediate_attention?(event_item)
  566. }
  567. end
  568. # 格式化审批决定信息
  569. def approval_decision_info
  570. {
  571. approved_by: user_info(admin_user),
  572. approved_at: Time.current,
  573. reason: @approval_options[:reason],
  574. notes: @approval_options[:notes],
  575. next_steps: get_next_steps_after_approval
  576. }
  577. end
  578. # 获取审批后的下一步操作
  579. def get_next_steps_after_approval
  580. if @action == :approve
  581. [
  582. "活动已进入报名状态",
  583. "系统已自动通知活动创建者",
  584. "参与者现在可以报名参加活动",
  585. "活动将在开始日期自动开始"
  586. ]
  587. else
  588. [
  589. "活动已被拒绝",
  590. "创建者可以根据拒绝理由修改活动",
  591. "修改后可以重新提交审批"
  592. ]
  593. end
  594. end
  595. # 检查活动是否需要立即关注
  596. def requires_immediate_attention?(event_item)
  597. # 检查是否即将开始
  598. return true if event_item.start_date && event_item.start_date <= Date.today + 3.days
  599. # 检查是否已经提交很久
  600. return true if event_item.submitted_for_approval_at &&
  601. (Time.current - event_item.submitted_for_approval_at) > 7.days
  602. false
  603. end
  604. # 格式化用户信息
  605. def user_info(user)
  606. return nil unless user
  607. {
  608. id: user.id,
  609. nickname: user.nickname,
  610. avatar_url: user.avatar_url
  611. }
  612. end
  613. def admin_user_info
  614. user_info(admin_user)
  615. end
  616. end

app/services/analytics_service.rb

0.0% lines covered

546 relevant lines. 0 lines covered and 546 lines missed.
    
  1. # 分析服务
  2. # 负责提供系统各方面的统计分析功能
  3. class AnalyticsService
  4. class << self
  5. # 获取系统总览统计
  6. def system_overview
  7. {
  8. users: user_statistics,
  9. events: event_statistics,
  10. check_ins: check_in_statistics,
  11. flowers: flower_statistics,
  12. notifications: notification_statistics,
  13. engagement: engagement_statistics
  14. }
  15. end
  16. # 用户统计
  17. def user_statistics
  18. {
  19. total_users: User.count,
  20. active_users_7_days: active_users_count(7.days.ago),
  21. active_users_30_days: active_users_count(30.days.ago),
  22. new_users_today: User.where('created_at >= ?', Date.current).count,
  23. new_users_7_days: User.where('created_at >= ?', 7.days.ago).count,
  24. new_users_30_days: User.where('created_at >= ?', 30.days.ago).count,
  25. user_roles: User.group(:role).count
  26. }
  27. end
  28. # 活动统计
  29. def event_statistics
  30. {
  31. total_events: ReadingEvent.count,
  32. active_events: ReadingEvent.where(status: ['enrolling', 'in_progress']).count,
  33. completed_events: ReadingEvent.where(status: 'completed').count,
  34. draft_events: ReadingEvent.where(status: 'draft').count,
  35. events_7_days: ReadingEvent.where('created_at >= ?', 7.days.ago).count,
  36. events_30_days: ReadingEvent.where('created_at >= ?', 30.days.ago).count,
  37. approval_stats: {
  38. pending: ReadingEvent.where(approval_status: 'pending').count,
  39. approved: ReadingEvent.where(approval_status: 'approved').count,
  40. rejected: ReadingEvent.where(approval_status: 'rejected').count
  41. },
  42. activity_modes: ReadingEvent.group(:activity_mode).count
  43. }
  44. end
  45. # 打卡统计
  46. def check_in_statistics
  47. all_check_ins = CheckIn.all
  48. # 计算质量分布(使用计算属性)
  49. quality_scores = all_check_ins.map(&:quality_score).compact
  50. quality_distribution = quality_scores.group_by { |score| score / 10 * 10 }.transform_values(&:count)
  51. {
  52. total_check_ins: all_check_ins.count,
  53. check_ins_today: all_check_ins.where('created_at >= ?', Date.current).count,
  54. check_ins_7_days: all_check_ins.where('created_at >= ?', 7.days.ago).count,
  55. check_ins_30_days: all_check_ins.where('created_at >= ?', 30.days.ago).count,
  56. quality_distribution: quality_distribution,
  57. status_distribution: all_check_ins.group(:status).count,
  58. average_word_count: all_check_ins.average(:word_count)&.round(2) || 0,
  59. high_quality_rate: ((all_check_ins.select(&:high_quality?).count.to_f / all_check_ins.count * 100).round(2) rescue 0)
  60. }
  61. end
  62. # 小红花统计
  63. def flower_statistics
  64. {
  65. total_flowers: Flower.count,
  66. flowers_today: Flower.where('created_at >= ?', Date.current).count,
  67. flowers_7_days: Flower.where('created_at >= ?', 7.days.ago).count,
  68. flowers_30_days: Flower.where('created_at >= ?', 30.days.ago).count,
  69. unique_givers: Flower.distinct.count(:giver_id),
  70. unique_receivers: Flower.distinct.count(:recipient_id),
  71. flower_types: Flower.group(:flower_type).count,
  72. average_amount: Flower.average(:amount)&.round(2) || 0,
  73. comments_count: Flower.where.not(comment: [nil, '']).count
  74. }
  75. end
  76. # 通知统计
  77. def notification_statistics
  78. {
  79. total_notifications: Notification.count,
  80. notifications_today: Notification.where('created_at >= ?', Date.current).count,
  81. notifications_7_days: Notification.where('created_at >= ?', 7.days.ago).count,
  82. notifications_30_days: Notification.where('created_at >= ?', 30.days.ago).count,
  83. unread_notifications: Notification.where(read: false).count,
  84. read_rate: ((Notification.where(read: true).count.to_f / Notification.count * 100).round(2) rescue 0),
  85. notification_types: Notification.group(:notification_type).count
  86. }
  87. end
  88. # 参与度统计
  89. def engagement_statistics
  90. {
  91. average_event_participants: average_participants_per_event,
  92. average_check_ins_per_event: average_check_ins_per_event,
  93. average_flowers_per_event: average_flowers_per_event,
  94. user_retention_rate: user_retention_rate,
  95. daily_active_users: daily_active_users_data(7.days.ago),
  96. popular_event_types: most_popular_event_types
  97. }
  98. end
  99. # 用户详细统计
  100. def user_analytics(user, days = 30)
  101. start_date = days.days.ago
  102. {
  103. profile: user_profile_stats(user, start_date),
  104. participation: user_participation_stats(user, start_date),
  105. engagement: user_engagement_stats(user, start_date),
  106. achievements: user_achievement_stats(user, start_date)
  107. }
  108. end
  109. # 活动详细统计
  110. def event_analytics(event)
  111. {
  112. overview: event_overview_stats(event),
  113. participation: event_participation_stats(event),
  114. engagement: event_engagement_stats(event),
  115. timeline: event_timeline_stats(event),
  116. feedback: event_feedback_stats(event)
  117. }
  118. end
  119. # 趋势数据
  120. def trend_data(metric, period = :week, days = 30)
  121. start_date = days.days.ago
  122. data_points = generate_time_points(start_date, period)
  123. data_points.map do |date|
  124. value = case metric
  125. when :check_ins
  126. CheckIn.where(created_at: date..end_of_period(date, period)).count
  127. when :flowers
  128. Flower.where(created_at: date..end_of_period(date, period)).count
  129. when :users
  130. User.where(created_at: date..end_of_period(date, period)).count
  131. when :events
  132. ReadingEvent.where(created_at: date..end_of_period(date, period)).count
  133. when :notifications
  134. Notification.where(created_at: date..end_of_period(date, period)).count
  135. else
  136. 0
  137. end
  138. {
  139. date: date.strftime('%Y-%m-%d'),
  140. value: value
  141. }
  142. end
  143. end
  144. # 排行榜数据
  145. def leaderboards(type = :flowers, limit = 10, period = :all_time)
  146. case type
  147. when :flowers
  148. flowers_leaderboard(limit, period)
  149. when :check_ins
  150. check_ins_leaderboard(limit, period)
  151. when :participation
  152. participation_leaderboard(limit, period)
  153. else
  154. []
  155. end
  156. end
  157. private
  158. # 活跃用户数量
  159. def active_users_count(since)
  160. User.joins(:check_ins)
  161. .where('check_ins.created_at >= ?', since)
  162. .distinct
  163. .count
  164. end
  165. # 每个活动的平均参与人数
  166. def average_participants_per_event
  167. return 0 if ReadingEvent.count == 0
  168. total_participants = ReadingEvent.joins(:event_enrollments)
  169. .where(event_enrollments: { status: 'enrolled' })
  170. .count
  171. (total_participants.to_f / ReadingEvent.count).round(2)
  172. end
  173. # 每个活动的平均打卡数
  174. def average_check_ins_per_event
  175. return 0 if ReadingEvent.count == 0
  176. total_check_ins = CheckIn.joins(reading_event: :event_enrollments)
  177. .count
  178. (total_check_ins.to_f / ReadingEvent.count).round(2)
  179. end
  180. # 每个活动的平均小红花数
  181. def average_flowers_per_event
  182. return 0 if ReadingEvent.count == 0
  183. total_flowers = Flower.joins(check_in: { reading_event: :event_enrollments })
  184. .count
  185. (total_flowers.to_f / ReadingEvent.count).round(2)
  186. end
  187. # 用户留存率
  188. def user_retention_rate
  189. new_users_30_days_ago = User.where('created_at BETWEEN ? AND ?', 60.days.ago, 30.days.ago)
  190. return 0 if new_users_30_days_ago.count == 0
  191. retained_users = new_users_30_days_ago.joins(:check_ins)
  192. .where('check_ins.created_at >= ?', 30.days.ago)
  193. .distinct
  194. .count
  195. (retained_users.to_f / new_users_30_days_ago.count * 100).round(2)
  196. end
  197. # 每日活跃用户数据
  198. def daily_active_users_data(since)
  199. (since.to_date..Date.current).map do |date|
  200. active_users = User.joins(:check_ins)
  201. .where('check_ins.created_at >= ? AND check_ins.created_at < ?',
  202. date.beginning_of_day, date.end_of_day)
  203. .distinct
  204. .count
  205. {
  206. date: date.strftime('%Y-%m-%d'),
  207. active_users: active_users
  208. }
  209. end
  210. end
  211. # 最受欢迎的活动类型
  212. def most_popular_event_types
  213. ReadingEvent.joins(:event_enrollments)
  214. .group('reading_events.activity_mode')
  215. .count('event_enrollments.id')
  216. .sort_by { |_, count| -count }
  217. .first(5)
  218. .map { |mode, count| { activity_mode: mode, participants: count } }
  219. end
  220. # 用户档案统计
  221. def user_profile_stats(user, start_date)
  222. {
  223. user_id: user.id,
  224. nickname: user.nickname,
  225. member_since: user.created_at,
  226. last_active: last_activity_date(user),
  227. participation_score: calculate_participation_score(user, start_date),
  228. influence_score: calculate_influence_score(user, start_date)
  229. }
  230. end
  231. # 用户参与统计
  232. def user_participation_stats(user, start_date)
  233. enrollments = user.event_enrollments.where('created_at >= ?', start_date)
  234. {
  235. events_enrolled: enrollments.count,
  236. events_completed: enrollments.where(status: 'completed').count,
  237. completion_rate: calculate_completion_rate(enrollments),
  238. check_ins_total: user.check_ins.where('created_at >= ?', start_date).count,
  239. average_check_ins_per_event: calculate_avg_check_ins_per_event(enrollments)
  240. }
  241. end
  242. # 用户互动统计
  243. def user_engagement_stats(user, start_date)
  244. {
  245. flowers_given: user.given_flowers.where('created_at >= ?', start_date).count,
  246. flowers_received: user.received_flowers.where('created_at >= ?', start_date).count,
  247. comments_given: user.comments.where('created_at >= ?', start_date).count,
  248. notifications_sent: Notification.where(actor: user).where('created_at >= ?', start_date).count,
  249. interaction_score: calculate_interaction_score(user, start_date)
  250. }
  251. end
  252. # 用户成就统计
  253. def user_achievement_stats(user, start_date)
  254. {
  255. certificates_count: user.flower_certificates.where('created_at >= ?', start_date).count,
  256. top_three_finishes: user.flower_certificates.where(rank: [1, 2, 3]).count,
  257. high_quality_check_ins: user.check_ins.where('created_at >= ?', start_date).select(&:high_quality?).count,
  258. streak_days: calculate_current_streak(user),
  259. achievements: get_user_achievements(user, start_date)
  260. }
  261. end
  262. # 活动概览统计
  263. def event_overview_stats(event)
  264. {
  265. event_id: event.id,
  266. title: event.title,
  267. status: event.status,
  268. approval_status: event.approval_status,
  269. created_at: event.created_at,
  270. start_date: event.start_date,
  271. end_date: event.end_date,
  272. duration_days: event.days_count,
  273. activity_mode: event.activity_mode
  274. }
  275. end
  276. # 活动参与统计
  277. def event_participation_stats(event)
  278. enrollments = event.event_enrollments
  279. {
  280. total_enrollments: enrollments.count,
  281. active_enrollments: enrollments.where(status: 'enrolled').count,
  282. completed_enrollments: enrollments.where(status: 'completed').count,
  283. completion_rate: calculate_event_completion_rate(enrollments),
  284. average_completion_rate: enrollments.average(:completion_rate)&.round(2) || 0,
  285. participation_trend: participation_trend_data(event)
  286. }
  287. end
  288. # 活动互动统计
  289. def event_engagement_stats(event)
  290. {
  291. total_check_ins: event.check_ins.count,
  292. unique_participants_checking_in: event.check_ins.distinct.count(:user_id),
  293. average_check_ins_per_participant: calculate_avg_check_ins(event),
  294. total_flowers: event.flowers_count,
  295. flowers_per_check_in: calculate_flowers_per_check_in(event),
  296. engagement_score: calculate_event_engagement_score(event)
  297. }
  298. end
  299. # 活动时间线统计
  300. def event_timeline_stats(event)
  301. if event.status == 'completed'
  302. {
  303. total_duration: event.days_count,
  304. actual_start_date: event.reading_schedules.minimum(:date),
  305. actual_end_date: event.reading_schedules.maximum(:date),
  306. peak_activity_day: find_peak_activity_day(event),
  307. daily_participation: daily_participation_data(event)
  308. }
  309. else
  310. {
  311. planned_duration: event.days_count,
  312. progress_percentage: calculate_event_progress(event),
  313. current_day: calculate_current_event_day(event),
  314. daily_activity: daily_activity_data(event)
  315. }
  316. end
  317. end
  318. # 活动反馈统计
  319. def event_feedback_stats(event)
  320. {
  321. average_rating: calculate_average_rating(event),
  322. feedback_count: count_feedback_responses(event),
  323. satisfaction_rate: calculate_satisfaction_rate(event),
  324. common_themes: analyze_feedback_themes(event)
  325. }
  326. end
  327. # 生成时间点
  328. def generate_time_points(start_date, period)
  329. case period
  330. when :day
  331. (start_date.to_date..Date.current).to_a
  332. when :week
  333. weeks = []
  334. current = start_date.to_date.beginning_of_week
  335. while current <= Date.current
  336. weeks << current
  337. current += 1.week
  338. end
  339. weeks
  340. when :month
  341. months = []
  342. current = start_date.to_date.beginning_of_month
  343. while current <= Date.current
  344. months << current
  345. current += 1.month
  346. end
  347. months
  348. else
  349. [start_date.to_date]
  350. end
  351. end
  352. # 期间的结束时间
  353. def end_of_period(date, period)
  354. case period
  355. when :day
  356. date.end_of_day
  357. when :week
  358. date.end_of_week
  359. when :month
  360. date.end_of_month
  361. else
  362. date.end_of_day
  363. end
  364. end
  365. # 小红花排行榜
  366. def flowers_leaderboard(limit, period)
  367. flowers_query = Flower.all
  368. case period
  369. when :today
  370. flowers_query = flowers_query.where('created_at >= ?', Date.current)
  371. when :week
  372. flowers_query = flowers_query.where('created_at >= ?', 1.week.ago)
  373. when :month
  374. flowers_query = flowers_query.where('created_at >= ?', 1.month.ago)
  375. end
  376. # 简化实现:先查询小红花,然后按用户分组统计
  377. user_flower_stats = {}
  378. flowers_query.includes(:recipient).find_each do |flower|
  379. recipient_id = flower.recipient_id
  380. user_flower_stats[recipient_id] ||= {
  381. user: flower.recipient,
  382. total_flowers: 0,
  383. flowers_count: 0
  384. }
  385. user_flower_stats[recipient_id][:total_flowers] += flower.amount || 1
  386. user_flower_stats[recipient_id][:flowers_count] += 1
  387. end
  388. # 按总数排序并限制数量
  389. user_flower_stats.values
  390. .sort_by { |stats| -stats[:total_flowers] }
  391. .first(limit)
  392. .map.with_index(1) do |stats, index|
  393. {
  394. user: stats[:user].as_json_for_api,
  395. total_flowers: stats[:total_flowers],
  396. flowers_count: stats[:flowers_count],
  397. rank: index
  398. }
  399. end
  400. end
  401. # 打卡排行榜
  402. def check_ins_leaderboard(limit, period)
  403. check_ins_query = CheckIn.all
  404. case period
  405. when :today
  406. check_ins_query = check_ins_query.where('created_at >= ?', Date.current)
  407. when :week
  408. check_ins_query = check_ins_query.where('created_at >= ?', 1.week.ago)
  409. when :month
  410. check_ins_query = check_ins_query.where('created_at >= ?', 1.month.ago)
  411. end
  412. # 简化实现:先查询打卡,然后按用户分组统计
  413. user_check_in_stats = {}
  414. check_ins_query.includes(:user).find_each do |check_in|
  415. user_id = check_in.user_id
  416. user_check_in_stats[user_id] ||= {
  417. user: check_in.user,
  418. check_ins_count: 0,
  419. quality_scores: []
  420. }
  421. user_check_in_stats[user_id][:check_ins_count] += 1
  422. user_check_in_stats[user_id][:quality_scores] << check_in.quality_score if check_in.quality_score
  423. end
  424. # 计算平均质量分并排序
  425. user_check_in_stats.values
  426. .map do |stats|
  427. avg_quality = if stats[:quality_scores].any?
  428. stats[:quality_scores].sum.to_f / stats[:quality_scores].count
  429. else
  430. 0
  431. end
  432. {
  433. user: stats[:user].as_json_for_api,
  434. check_ins_count: stats[:check_ins_count],
  435. average_quality: avg_quality.round(2),
  436. rank: 0
  437. }
  438. end
  439. .sort_by { |stats| [-stats[:check_ins_count], -stats[:average_quality]] }
  440. .first(limit)
  441. .map.with_index(1) { |stats, index| stats.merge(rank: index) }
  442. end
  443. # 参与度排行榜
  444. def participation_leaderboard(limit, period)
  445. enrollments_query = EventEnrollment.all
  446. case period
  447. when :today
  448. enrollments_query = enrollments_query.where('created_at >= ?', Date.current)
  449. when :week
  450. enrollments_query = enrollments_query.where('created_at >= ?', 1.week.ago)
  451. when :month
  452. enrollments_query = enrollments_query.where('created_at >= ?', 1.month.ago)
  453. end
  454. # 简化实现:先查询报名,然后按用户分组统计
  455. user_enrollment_stats = {}
  456. enrollments_query.includes(:user).find_each do |enrollment|
  457. user_id = enrollment.user_id
  458. event_id = enrollment.reading_event_id
  459. completion_rate = enrollment.completion_rate || 0
  460. user_enrollment_stats[user_id] ||= {
  461. user: enrollment.user,
  462. events_count: 0,
  463. event_ids: Set.new,
  464. completion_rates: []
  465. }
  466. user_enrollment_stats[user_id][:events_count] += 1
  467. user_enrollment_stats[user_id][:event_ids] << event_id
  468. user_enrollment_stats[user_id][:completion_rates] << completion_rate
  469. end
  470. # 计算统计数据并排序
  471. user_enrollment_stats.values
  472. .map do |stats|
  473. unique_events = stats[:event_ids].size
  474. avg_completion = if stats[:completion_rates].any?
  475. stats[:completion_rates].sum.to_f / stats[:completion_rates].count
  476. else
  477. 0
  478. end
  479. {
  480. user: stats[:user].as_json_for_api,
  481. events_count: unique_events,
  482. average_completion: avg_completion.round(2),
  483. rank: 0
  484. }
  485. end
  486. .sort_by { |stats| [-stats[:events_count], -stats[:average_completion]] }
  487. .first(limit)
  488. .map.with_index(1) { |stats, index| stats.merge(rank: index) }
  489. end
  490. # 辅助方法(计算各种指标)
  491. def last_activity_date(user)
  492. user.check_ins.maximum(:created_at) ||
  493. user.flowers.maximum(:created_at) ||
  494. user.comments.maximum(:created_at) ||
  495. user.created_at
  496. end
  497. def calculate_participation_score(user, start_date)
  498. # 基于活动参与、打卡次数、完成率等计算
  499. enrollments = user.event_enrollments.where('created_at >= ?', start_date)
  500. check_ins = user.check_ins.where('created_at >= ?', start_date)
  501. base_score = enrollments.count * 10
  502. check_in_score = check_ins.count * 5
  503. completion_bonus = enrollments.where(status: 'completed').count * 20
  504. base_score + check_in_score + completion_bonus
  505. end
  506. def calculate_influence_score(user, start_date)
  507. # 基于小红花、评论、通知等计算影响力
  508. flowers_given = user.given_flowers.where('created_at >= ?', start_date).count
  509. comments = user.comments.where('created_at >= ?', start_date).count
  510. flowers_given * 15 + comments * 10
  511. end
  512. def calculate_completion_rate(enrollments)
  513. return 0 if enrollments.empty?
  514. completed = enrollments.where(status: 'completed').count
  515. (completed.to_f / enrollments.count * 100).round(2)
  516. end
  517. def calculate_avg_check_ins_per_event(enrollments)
  518. return 0 if enrollments.empty?
  519. # 避免JOIN查询中的列名冲突,改用子查询
  520. check_in_ids = CheckIn.where(enrollment_id: enrollments.pluck(:id)).pluck(:id)
  521. total_check_ins = check_in_ids.count
  522. (total_check_ins.to_f / enrollments.count).round(2)
  523. end
  524. def calculate_interaction_score(user, start_date)
  525. flowers_received = user.received_flowers.where('created_at >= ?', start_date).count
  526. # 这里简化处理,因为 Comment 可能不直接与 User 关联
  527. comments_received = 0 # Comment.where(commentable: user).where('created_at >= ?', start_date).count
  528. flowers_received * 10 + comments_received * 5
  529. end
  530. def calculate_current_streak(user)
  531. # 计算用户当前的打卡连续天数
  532. # 这里简化实现,实际可以更复杂
  533. recent_check_ins = user.check_ins.where('created_at >= ?', 30.days.ago)
  534. .order(created_at: :desc)
  535. return 0 if recent_check_ins.empty?
  536. streak = 0
  537. current_date = Date.current
  538. recent_check_ins.each do |check_in|
  539. if check_in.created_at.to_date == current_date
  540. streak += 1
  541. current_date -= 1.day
  542. else
  543. break
  544. end
  545. end
  546. streak
  547. end
  548. def get_user_achievements(user, start_date)
  549. # 获取用户成就徽章等
  550. achievements = []
  551. # 基于各种条件授予成就
  552. if user.check_ins.where('created_at >= ?', start_date).count >= 30
  553. achievements << { name: '勤奋打卡', description: '30天内打卡超过30次' }
  554. end
  555. if user.received_flowers.where('created_at >= ?', start_date).count >= 10
  556. achievements << { name: '人气之星', description: '30天内收到超过10朵小红花' }
  557. end
  558. achievements
  559. end
  560. def calculate_event_completion_rate(enrollments)
  561. return 0 if enrollments.empty?
  562. completed = enrollments.where(status: 'completed').count
  563. (completed.to_f / enrollments.count * 100).round(2)
  564. end
  565. def calculate_avg_check_ins(event)
  566. return 0 if event.event_enrollments.empty?
  567. total_check_ins = event.check_ins.count
  568. (total_check_ins.to_f / event.event_enrollments.count).round(2)
  569. end
  570. def calculate_flowers_per_check_in(event)
  571. check_ins_count = event.check_ins.count
  572. flowers_count = event.flowers_count
  573. return 0 if check_ins_count == 0
  574. (flowers_count.to_f / check_ins_count).round(2)
  575. end
  576. def calculate_event_engagement_score(event)
  577. # 综合评分:打卡率 + 小红花率 + 完成率
  578. check_in_rate = [calculate_avg_check_ins(event) * 10, 100].min
  579. flower_rate = [calculate_flowers_per_check_in(event) * 20, 100].min
  580. completion_rate = calculate_event_completion_rate(event.event_enrollments)
  581. (check_in_rate * 0.4 + flower_rate * 0.3 + completion_rate * 0.3).round(2)
  582. end
  583. def calculate_event_progress(event)
  584. return 100 if event.status == 'completed'
  585. return 0 if event.status == 'draft'
  586. total_days = event.days_count
  587. return 0 if total_days == 0
  588. elapsed_days = [(Date.current - event.start_date).to_i, 0].max
  589. [elapsed_days.to_f / total_days * 100, 100].min.round(2)
  590. end
  591. def calculate_current_event_day(event)
  592. return 0 if event.start_date.nil?
  593. elapsed = [(Date.current - event.start_date).to_i + 1, 1].max
  594. [elapsed, event.days_count].min
  595. end
  596. # 更多辅助方法可以根据需要继续添加...
  597. end
  598. end

app/services/api_performance_service.rb

0.0% lines covered

265 relevant lines. 0 lines covered and 265 lines missed.
    
  1. # frozen_string_literal: true
  2. # API性能优化服务
  3. # 提供API请求限流、分页优化、批量操作等功能
  4. class ApiPerformanceService
  5. class << self
  6. # API请求限流
  7. # @param identifier [String] 请求标识符(用户ID、IP地址等)
  8. # @param limit [Integer] 请求限制数量
  9. # @param period [Integer] 时间周期(秒)
  10. # @param cache_key_prefix [String] 缓存键前缀
  11. # @return [Hash] 限流结果
  12. def rate_limit(identifier, limit: 100, period: 60, cache_key_prefix: 'rate_limit')
  13. cache_key = "#{cache_key_prefix}:#{identifier}:#{Time.current.to_i / period}"
  14. current_count = Rails.cache.read(cache_key) || 0
  15. if current_count >= limit
  16. {
  17. allowed: false,
  18. remaining: 0,
  19. reset_time: (Time.current.to_i / period + 1) * period,
  20. retry_after: period - (Time.current.to_i % period)
  21. }
  22. else
  23. # 增加计数
  24. Rails.cache.write(cache_key, current_count + 1, expires_in: period)
  25. {
  26. allowed: true,
  27. remaining: limit - current_count - 1,
  28. reset_time: (Time.current.to_i / period + 1) * period,
  29. current_count: current_count + 1
  30. }
  31. end
  32. end
  33. # 智能API响应格式化
  34. # @param success [Boolean] 请求是否成功
  35. # @param data [Object] 响应数据
  36. # @param message [String] 响应消息
  37. # @param meta [Hash] 元数据
  38. # @param status_code [Integer] HTTP状态码
  39. # @return [Hash] 格式化的API响应
  40. def format_api_response(success: true, data: nil, message: nil, meta: {}, status_code: 200)
  41. response = {
  42. success: success,
  43. message: message,
  44. data: data,
  45. meta: meta
  46. }
  47. # 添加时间戳
  48. response[:timestamp] = Time.current.iso8601
  49. # 添加请求ID(如果存在)
  50. if RequestStore.store[:request_id]
  51. response[:request_id] = RequestStore.store[:request_id]
  52. end
  53. response
  54. end
  55. # 优化的分页响应
  56. # @param pagination_result [Hash] 分页结果
  57. # @param additional_meta [Hash] 额外的元数据
  58. # @return [Hash] 格式化的分页响应
  59. def format_paginated_response(pagination_result, additional_meta = {})
  60. meta = pagination_result[:pagination] || {}
  61. meta.merge!(additional_meta)
  62. format_api_response(
  63. success: true,
  64. data: pagination_result[:records],
  65. meta: meta
  66. )
  67. end
  68. # 批量操作支持
  69. # @param records [Array] 记录数组
  70. # @param batch_size [Integer] 批次大小
  71. # @param options [Hash] 操作选项
  72. # @yield [Array] 每批记录
  73. # @return [Array] 所有操作结果
  74. def batch_process(records, batch_size: 50, options = {})
  75. return [] if records.empty?
  76. results = []
  77. total_batches = (records.length.to_f / batch_size).ceil
  78. current_batch = 1
  79. records.each_slice(batch_size) do |batch|
  80. begin
  81. batch_result = yield(batch) if block_given?
  82. if batch_result.is_a?(Array)
  83. results.concat(batch_result)
  84. else
  85. results << batch_result
  86. end
  87. # 批次操作日志
  88. Rails.logger.info "批量操作进度: #{current_batch}/#{total_batches} 批次完成" if options[:log_progress]
  89. rescue => e
  90. Rails.logger.error "批量操作失败 (批次 #{current_batch}): #{e.message}"
  91. if options[:continue_on_error]
  92. results << { error: e.message, batch: current_batch }
  93. else
  94. raise e
  95. end
  96. end
  97. current_batch += 1
  98. end
  99. results
  100. end
  101. # API字段选择器
  102. # @param records [Array] 记录数组
  103. # @param fields [Array] 需要返回的字段
  104. # @param options [Hash] 选项
  105. # @return [Array] 选择字段后的记录
  106. def select_fields(records, fields, options = {})
  107. return records if fields.blank? || records.empty?
  108. records.map do |record|
  109. if record.respond_to?(:as_json_for_api)
  110. # 如果记录支持as_json_for_api方法
  111. record.as_json_for_api(options.slice(:includes))
  112. else
  113. record.as_json
  114. end.slice(*fields.map(&:to_s))
  115. end
  116. end
  117. # 数据压缩(对大型响应进行压缩)
  118. # @param data [Hash] 要压缩的数据
  119. # @param threshold [Integer] 压缩阈值(字符数)
  120. # @return [Hash] 压缩后的数据
  121. def compress_if_needed(data, threshold: 10240) # 10KB
  122. return data unless should_compress?(data, threshold)
  123. compressed_data = {
  124. compressed: true,
  125. data: compress_data(data),
  126. original_size: data.to_s.length,
  127. compressed_size: compress_data(data).length
  128. }
  129. end
  130. # API缓存装饰器
  131. # @param cache_key [String] 缓存键
  132. # @param ttl [Integer] 缓存时间(秒)
  133. # @param options [Hash] 缓存选项
  134. # @yield 要缓存的操作
  135. # @return [Object] 缓存的结果
  136. def cache_api_response(cache_key, ttl: 5.minutes, options = {})
  137. # 如果用户未登录,不缓存
  138. return yield unless options[:skip_auth_check] || RequestStore.store[:current_user]
  139. full_cache_key = "api_response:#{cache_key}:#{RequestStore.store[:current_user]&.id}:#{RequestStore.store[:user_role]}"
  140. Rails.cache.fetch(full_cache_key, expires_in: ttl, race_condition_ttl: 30.seconds) do
  141. yield
  142. end
  143. end
  144. # API性能监控
  145. # @param endpoint [String] API端点
  146. # @param method [String] HTTP方法
  147. # @param options [Hash] 监控选项
  148. # @yield 要监控的操作
  149. # @return [Object] 操作结果
  150. def monitor_performance(endpoint, method: 'GET', options = {})
  151. start_time = Time.current
  152. begin
  153. result = yield
  154. execution_time = Time.current - start_time
  155. log_performance_metrics(endpoint, method, execution_time, options, true)
  156. result
  157. rescue => e
  158. execution_time = Time.current - start_time
  159. log_performance_metrics(endpoint, method, execution_time, options, false, e)
  160. raise e
  161. end
  162. end
  163. # 请求参数验证和清理
  164. # @param params [Hash] 请求参数
  165. # @param allowed_params [Array] 允许的参数列表
  166. # @param options [Hash] 验证选项
  167. # @return [Hash] 清理后的参数
  168. def sanitize_params(params, allowed_params, options = {})
  169. return {} if params.blank?
  170. # 只保留允许的参数
  171. sanitized = params.slice(*allowed_params)
  172. # 类型转换
  173. sanitized = convert_param_types(sanitized, options[:type_conversions] || {})
  174. # 验证必填参数
  175. if options[:required]&.any?
  176. missing_params = options[:required] - sanitized.keys
  177. if missing_params.any?
  178. raise ArgumentError, "缺少必填参数: #{missing_params.join(', ')}"
  179. end
  180. end
  181. # 参数值验证
  182. if options[:validations]
  183. validate_param_values(sanitized, options[:validations])
  184. end
  185. sanitized
  186. end
  187. # 响应时间优化:异步处理非关键操作
  188. # @param operation [Symbol] 操作类型
  189. # @param data [Object] 操作数据
  190. # @param options [Hash] 操作选项
  191. def async_process(operation, data, options = {})
  192. # 使用Rails的Active Job进行异步处理
  193. case operation
  194. when :send_notification
  195. NotificationJob.perform_later(data, options)
  196. when :update_statistics
  197. StatisticsJob.perform_later(data, options)
  198. when :send_email
  199. EmailJob.perform_later(data, options)
  200. when :generate_report
  201. ReportJob.perform_later(data, options)
  202. else
  203. Rails.logger.warn "未知的异步操作类型: #{operation}"
  204. end
  205. end
  206. # 响应压缩中间件支持
  207. # @param response_body [String] 响应体
  208. # @param request_headers [Hash] 请求头
  209. # @return [String] 压缩后的响应体
  210. def compress_response(response_body, request_headers = {})
  211. # 检查客户端是否支持压缩
  212. accept_encoding = request_headers['Accept-Encoding'] || ''
  213. return response_body unless accept_encoding.include?('gzip')
  214. # 压缩响应
  215. require 'zlib'
  216. compressed = Zlib::Deflate.deflate(response_body)
  217. # 添加压缩标记
  218. response_body
  219. end
  220. # API版本控制支持
  221. # @param request [ActionDispatch::Request] 请求对象
  222. # @param available_versions [Array] 可用的API版本
  223. # @return [String] 选择的API版本
  224. def determine_api_version(request, available_versions = ['v1'])
  225. # 从URL路径获取版本
  226. version_from_path = request.path.split('/')[1]
  227. return version_from_path if available_versions.include?(version_from_path)
  228. # 从请求头获取版本
  229. version_from_header = request.headers['API-Version']
  230. return version_from_header if available_versions.include?(version_from_header)
  231. # 返回默认版本
  232. available_versions.first
  233. end
  234. # 响应格式协商
  235. # @param request [ActionDispatch::Request] 请求对象
  236. # @param data [Object] 响应数据
  237. # @param default_format [Symbol] 默认格式
  238. # @return [String] 格式化后的响应
  239. def negotiate_response_format(request, data, default_format = :json)
  240. accept_header = request.headers['Accept'] || 'application/json'
  241. case accept_header
  242. when /json/
  243. data.to_json
  244. when /xml/
  245. data.respond_to?(:to_xml) ? data.to_xml : data.to_json
  246. when /text/
  247. data.to_s
  248. else
  249. case default_format
  250. when :json
  251. data.to_json
  252. when :xml
  253. data.respond_to?(:to_xml) ? data.to_xml : data.to_json
  254. else
  255. data.to_s
  256. end
  257. end
  258. end
  259. private
  260. # 判断是否需要压缩
  261. def should_compress?(data, threshold)
  262. data.to_s.length > threshold
  263. end
  264. # 压缩数据
  265. def compress_data(data)
  266. require 'zlib'
  267. Base64.strict_encode64(Zlib::Deflate.deflate(data.to_json))
  268. end
  269. # 记录性能指标
  270. def log_performance_metrics(endpoint, method, execution_time, options, success, error = nil)
  271. metrics = {
  272. endpoint: endpoint,
  273. method: method,
  274. execution_time: execution_time.round(3),
  275. success: success,
  276. timestamp: Time.current.iso8601,
  277. user_id: RequestStore.store[:current_user]&.id,
  278. user_role: RequestStore.store[:user_role]
  279. }
  280. if error
  281. metrics[:error] = {
  282. message: error.message,
  283. class: error.class.name
  284. }
  285. end
  286. # 记录到日志
  287. if success && execution_time > 1.0
  288. Rails.logger.warn "慢查询警告: #{metrics}"
  289. elsif !success
  290. Rails.logger.error "API错误: #{metrics}"
  291. end
  292. # 发送到监控系统(如果配置了)
  293. if defined?(StatsD)
  294. StatsD.timing("api.#{endpoint.gsub('/', '_')}.#{method.downcase}", execution_time * 1000)
  295. StatsD.increment("api.#{endpoint.gsub('/', '_')}.#{method.downcase}.#{success ? 'success' : 'error'}")
  296. end
  297. end
  298. # 参数类型转换
  299. def convert_param_types(params, type_conversions)
  300. converted = params.dup
  301. type_conversions.each do |key, type|
  302. next unless converted.key?(key)
  303. case type
  304. when :integer
  305. converted[key] = converted[key].to_i
  306. when :float
  307. converted[key] = converted[key].to_f
  308. when :boolean
  309. converted[key] = %w[true yes 1 t].include?(converted[key].to_s.downcase)
  310. when :date
  311. converted[key] = Date.parse(converted[key]) rescue nil
  312. when :datetime
  313. converted[key] = DateTime.parse(converted[key]) rescue nil
  314. end
  315. end
  316. converted
  317. end
  318. # 参数值验证
  319. def validate_param_values(params, validations)
  320. validations.each do |key, rules|
  321. next unless params.key?(key)
  322. value = params[key]
  323. # 必填验证
  324. if rules[:required] && value.blank?
  325. raise ArgumentError, "参数 #{key} 是必填的"
  326. end
  327. # 范围验证
  328. if rules[:range] && value.present?
  329. min_val, max_val = rules[:range]
  330. if min_val && value < min_val
  331. raise ArgumentError, "参数 #{key} 不能小于 #{min_val}"
  332. end
  333. if max_val && value > max_val
  334. raise ArgumentError, "参数 #{key} 不能大于 #{max_val}"
  335. end
  336. end
  337. # 长度验证
  338. if rules[:length] && value.present?
  339. min_len, max_len = rules[:length]
  340. if min_len && value.to_s.length < min_len
  341. raise ArgumentError, "参数 #{key} 长度不能小于 #{min_len}"
  342. end
  343. if max_len && value.to_s.length > max_len
  344. raise ArgumentError, "参数 #{key} 长度不能大于 #{max_len}"
  345. end
  346. end
  347. # 正则表达式验证
  348. if rules[:format] && value.present?
  349. regex = rules[:format].is_a?(Regexp) ? rules[:format] : Regexp.new(rules[:format])
  350. unless value.to_s.match?(regex)
  351. raise ArgumentError, "参数 #{key} 格式不正确"
  352. end
  353. end
  354. # 枚举值验证
  355. if rules[:in] && value.present?
  356. unless rules[:in].include?(value)
  357. raise ArgumentError, "参数 #{key} 必须是以下值之一: #{rules[:in].join(', ')}"
  358. end
  359. end
  360. end
  361. end
  362. end
  363. end

app/services/api_rate_limiting_service.rb

0.0% lines covered

227 relevant lines. 0 lines covered and 227 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApiRateLimitingService - API限流服务
  3. # 提供基于Redis的API请求限流功能
  4. class ApiRateLimitingService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :identifier, :limit, :window, :endpoint, :request
  7. def initialize(identifier:, limit: 100, window: 1.hour, endpoint: nil, request: nil)
  8. super()
  9. @identifier = identifier
  10. @limit = limit
  11. @window = window
  12. @endpoint = endpoint
  13. @request = request
  14. end
  15. def call
  16. handle_errors do
  17. check_rate_limit
  18. end
  19. self
  20. end
  21. def allowed?
  22. @allowed
  23. end
  24. def remaining_requests
  25. @remaining_requests
  26. end
  27. def reset_time
  28. @reset_time
  29. end
  30. def current_usage
  31. @current_usage
  32. end
  33. # 类方法:检查用户限流
  34. def self.check_user_rate_limit(user, endpoint: nil, request: nil)
  35. identifier = "user:#{user.id}"
  36. limit = rate_limit_for_user(user, endpoint)
  37. window = rate_window_for_user(user, endpoint)
  38. new(
  39. identifier: identifier,
  40. limit: limit,
  41. window: window,
  42. endpoint: endpoint,
  43. request: request
  44. ).call
  45. end
  46. # 类方法:检查IP限流
  47. def self.check_ip_rate_limit(ip_address, endpoint: nil, request: nil)
  48. identifier = "ip:#{ip_address}"
  49. limit = rate_limit_for_ip(ip_address, endpoint)
  50. window = rate_window_for_ip(ip_address, endpoint)
  51. new(
  52. identifier: identifier,
  53. limit: limit,
  54. window: window,
  55. endpoint: endpoint,
  56. request: request
  57. ).call
  58. end
  59. # 类方法:检查全局限流
  60. def self.check_global_rate_limit(endpoint: nil, request: nil)
  61. identifier = "global"
  62. limit = rate_limit_for_global(endpoint)
  63. window = rate_window_for_global(endpoint)
  64. new(
  65. identifier: identifier,
  66. limit: limit,
  67. window: window,
  68. endpoint: endpoint,
  69. request: request
  70. ).call
  71. end
  72. private
  73. def check_rate_limit
  74. # 使用Redis滑动窗口算法
  75. redis_key = build_redis_key
  76. # 获取当前时间窗口内的请求计数
  77. current_time = Time.current.to_f
  78. window_start = current_time - window.to_f
  79. # 清理过期的请求记录
  80. cleanup_expired_requests(redis_key, window_start)
  81. # 获取当前窗口内的请求数量
  82. @current_usage = get_current_usage(redis_key)
  83. @remaining_requests = [limit - @current_usage, 0].max
  84. @reset_time = calculate_reset_time(redis_key)
  85. # 检查是否超过限制
  86. if @current_usage >= limit
  87. @allowed = false
  88. log_rate_limit_violation
  89. add_error(:rate_limit_exceeded, "API请求频率超过限制,请稍后重试")
  90. else
  91. @allowed = true
  92. record_request(redis_key, current_time)
  93. end
  94. end
  95. def build_redis_key
  96. key_parts = ['rate_limit', identifier]
  97. key_parts << endpoint if endpoint
  98. key_parts.join(':')
  99. end
  100. def cleanup_expired_requests(redis_key, window_start)
  101. # 使用Redis有序集合,按时间戳存储请求
  102. redis.zremrangebyscore(redis_key, 0, window_start)
  103. end
  104. def get_current_usage(redis_key)
  105. redis.zcard(redis_key)
  106. end
  107. def record_request(redis_key, current_time)
  108. # 记录当前请求
  109. redis.zadd(redis_key, current_time, generate_request_id(current_time))
  110. # 设置键的过期时间
  111. redis.expire(redis_key, window.to_i)
  112. end
  113. def calculate_reset_time(redis_key)
  114. # 获取最早的请求时间
  115. earliest_request = redis.zrange(redis_key, 0, 0, withscores: true)
  116. if earliest_request.any?
  117. earliest_timestamp = earliest_request.first[1].to_f
  118. reset_timestamp = earliest_timestamp + window.to_f
  119. Time.at(reset_timestamp).iso8601
  120. else
  121. (Time.current + window).iso8601
  122. end
  123. end
  124. def generate_request_id(current_time)
  125. "#{current_time}_#{SecureRandom.hex(8)}"
  126. end
  127. def log_rate_limit_violation
  128. Rails.logger.warn(
  129. "Rate limit exceeded",
  130. {
  131. identifier: identifier,
  132. endpoint: endpoint,
  133. limit: limit,
  134. window: window,
  135. current_usage: @current_usage,
  136. ip: request&.remote_ip,
  137. user_agent: request&.user_agent
  138. }
  139. )
  140. end
  141. def redis
  142. @redis ||= Rails.cache.redis || Redis.new
  143. end
  144. # 类方法:定义不同角色的限流规则
  145. def self.rate_limit_for_user(user, endpoint = nil)
  146. return 1000 if user&.admin? # 管理员:1000次/小时
  147. return 500 if user&.vip? # VIP用户:500次/小时
  148. return 200 if user&.premium? # 高级用户:200次/小时
  149. 100 # 普通用户:100次/小时
  150. end
  151. def self.rate_window_for_user(user, endpoint = nil)
  152. return 5.minutes if user&.admin?
  153. 1.hour
  154. end
  155. def self.rate_limit_for_ip(ip_address, endpoint = nil)
  156. # 检查是否为可信IP
  157. return 1000 if trusted_ip?(ip_address)
  158. 50 # 默认:50次/分钟
  159. end
  160. def self.rate_window_for_ip(ip_address, endpoint = nil)
  161. return 5.minutes if trusted_ip?(ip_address)
  162. 1.minute
  163. end
  164. def self.rate_limit_for_global(endpoint = nil)
  165. case endpoint
  166. when /auth/
  167. 20 # 认证相关:20次/分钟
  168. when /upload/
  169. 10 # 上传相关:10次/分钟
  170. else
  171. 1000 # 全局:1000次/秒
  172. end
  173. end
  174. def self.rate_window_for_global(endpoint = nil)
  175. case endpoint
  176. when /auth/
  177. 1.minute
  178. when /upload/
  179. 1.minute
  180. else
  181. 1.second
  182. end
  183. end
  184. def self.trusted_ip?(ip_address)
  185. # 可信IP列表(可以来自配置或数据库)
  186. trusted_ips = Rails.application.config.x.trusted_ips || []
  187. trusted_ips.include?(ip_address)
  188. end
  189. # 获取限流统计信息
  190. def self.rate_limit_stats(identifier, window = 1.hour)
  191. redis_key = "rate_limit:#{identifier}"
  192. current_time = Time.current.to_f
  193. window_start = current_time - window.to_f
  194. # 清理过期记录
  195. Rails.cache.redis&.zremrangebyscore(redis_key, 0, window_start)
  196. # 获取统计数据
  197. {
  198. current_usage: Rails.cache.redis&.zcard(redis_key) || 0,
  199. requests_in_window: get_requests_in_window(redis_key, window_start, current_time),
  200. peak_usage: get_peak_usage(redis_key),
  201. average_usage: calculate_average_usage(redis_key, window)
  202. }
  203. end
  204. def self.get_requests_in_window(redis_key, window_start, current_time)
  205. requests = Rails.cache.redis&.zrangebyscore(redis_key, window_start, current_time) || []
  206. requests.map { |req| req.split('_').first.to_f }
  207. end
  208. def self.get_peak_usage(redis_key)
  209. # 这里可以实现更复杂的峰值统计逻辑
  210. Rails.cache.redis&.zcard(redis_key) || 0
  211. end
  212. def self.calculate_average_usage(redis_key, window)
  213. total_requests = Rails.cache.redis&.zcard(redis_key) || 0
  214. (total_requests.to_f / window).round(2)
  215. end
  216. # 重置用户限流计数器
  217. def self.reset_user_rate_limit(user_id)
  218. pattern = "rate_limit:user:#{user_id}:*"
  219. keys = Rails.cache.redis&.keys(pattern) || []
  220. keys.each do |key|
  221. Rails.cache.redis&.del(key)
  222. end
  223. end
  224. # 重置IP限流计数器
  225. def self.reset_ip_rate_limit(ip_address)
  226. pattern = "rate_limit:ip:#{ip_address}:*"
  227. keys = Rails.cache.redis&.keys(pattern) || []
  228. keys.each do |key|
  229. Rails.cache.redis&.del(key)
  230. end
  231. end
  232. # 获取限流配置信息
  233. def self.rate_limit_config
  234. {
  235. user_limits: {
  236. admin: { limit: 1000, window: '5 minutes' },
  237. vip: { limit: 500, window: '1 hour' },
  238. premium: { limit: 200, window: '1 hour' },
  239. regular: { limit: 100, window: '1 hour' }
  240. },
  241. ip_limits: {
  242. trusted: { limit: 1000, window: '5 minutes' },
  243. regular: { limit: 50, window: '1 minute' }
  244. },
  245. global_limits: {
  246. auth: { limit: 20, window: '1 minute' },
  247. upload: { limit: 10, window: '1 minute' },
  248. default: { limit: 1000, window: '1 second' }
  249. }
  250. }
  251. end
  252. end

app/services/api_response_service.rb

0.0% lines covered

182 relevant lines. 0 lines covered and 182 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApiResponseService - API响应标准化服务
  3. # 提供统一的API响应格式,包括成功响应、错误响应、分页响应等
  4. class ApiResponseService
  5. include ActionView::Helpers::NumberHelper
  6. # 尝试加载 RequestStore,如果不存在则跳过
  7. begin
  8. require 'request_store'
  9. rescue LoadError
  10. # RequestStore gem 没有安装,使用简单的替代方案
  11. end
  12. class << self
  13. # 标准成功响应
  14. # @param data [Object] 响应数据
  15. # @param message [String] 响应消息
  16. # @param meta [Hash] 元数据
  17. # @param status_code [Integer] HTTP状态码
  18. # @return [Hash] 标准化的响应格式
  19. def success_response(data: nil, message: '操作成功', meta: {}, status_code: 200)
  20. response = {
  21. success: true,
  22. message: message,
  23. data: data,
  24. meta: standard_meta(meta),
  25. timestamp: Time.current.iso8601
  26. }
  27. # 添加请求ID(如果存在)
  28. add_request_id(response)
  29. [response, status_code]
  30. end
  31. # 标准错误响应
  32. # @param message [String] 错误消息
  33. # @param error_code [String] 错误代码
  34. # @param details [Hash] 错误详情
  35. # @param status_code [Integer] HTTP状态码
  36. # @return [Hash] 标准化的错误响应格式
  37. def error_response(message: '操作失败', error_code: nil, details: {}, status_code: 400)
  38. response = {
  39. success: false,
  40. message: message,
  41. error_code: error_code,
  42. data: nil,
  43. meta: standard_meta,
  44. timestamp: Time.current.iso8601
  45. }
  46. # 添加错误详情(开发环境)
  47. if Rails.env.development? && details.any?
  48. response[:details] = details
  49. end
  50. # 添加请求ID(如果存在)
  51. add_request_id(response)
  52. [response, status_code]
  53. end
  54. # 验证错误响应
  55. # @param errors [ActiveModel::Errors] 验证错误对象
  56. # @param message [String] 响应消息
  57. # @return [Hash] 标准化的验证错误响应格式
  58. def validation_error_response(errors, message: '请求参数验证失败')
  59. error_details = if errors.respond_to?(:details)
  60. errors.details.transform_values do |details|
  61. details.map { |detail| detail[:error].to_s.humanize }
  62. end
  63. else
  64. errors.is_a?(Hash) ? errors : { base: [errors.to_s] }
  65. end
  66. error_response(
  67. message: message,
  68. error_code: 'validation_error',
  69. details: { errors: error_details },
  70. status_code: 422
  71. )
  72. end
  73. # 未找到错误响应
  74. # @param resource_type [String] 资源类型
  75. # @param resource_id [String, Integer] 资源ID
  76. # @return [Hash] 标准化的未找到响应格式
  77. def not_found_response(resource_type: '资源', resource_id: nil)
  78. message = if resource_id
  79. "#{resource_type} (ID: #{resource_id}) 不存在"
  80. else
  81. "#{resource_type} 不存在"
  82. end
  83. error_response(
  84. message: message,
  85. error_code: 'not_found',
  86. status_code: 404
  87. )
  88. end
  89. # 权限错误响应
  90. # @param message [String] 错误消息
  91. # @param required_permission [String] 需要的权限
  92. # @return [Hash] 标准化的权限错误响应格式
  93. def authorization_error_response(message: '权限不足', required_permission: nil)
  94. details = {}
  95. details[:required_permission] = required_permission if required_permission
  96. error_response(
  97. message: message,
  98. error_code: 'authorization_error',
  99. details: details,
  100. status_code: 403
  101. )
  102. end
  103. # 认证错误响应
  104. # @param message [String] 错误消息
  105. # @param details [Hash] 错误详情
  106. # @return [Hash] 标准化的认证错误响应格式
  107. def authentication_error_response(message: '认证失败', details: {})
  108. error_response(
  109. message: message,
  110. error_code: 'authentication_error',
  111. details: details,
  112. status_code: 401
  113. )
  114. end
  115. # 服务不可用错误响应
  116. # @param service_name [String] 服务名称
  117. # @param retry_after [Integer] 建议重试时间(秒)
  118. # @return [Hash] 标准化的服务不可用响应格式
  119. def service_unavailable_response(service_name: '服务', retry_after: 30)
  120. message = "#{service_name}暂时不可用,请稍后再试"
  121. response, = error_response(
  122. message: message,
  123. error_code: 'service_unavailable',
  124. status_code: 503
  125. )
  126. # 添加重试信息
  127. response[:meta][:retry_after] = retry_after
  128. [response, 503]
  129. end
  130. # 限流错误响应
  131. # @param limit_info [Hash] 限流信息
  132. # @return [Hash] 标准化的限流错误响应格式
  133. def rate_limit_error_response(limit_info = {})
  134. message = '请求过于频繁,请稍后再试'
  135. response, = error_response(
  136. message: message,
  137. error_code: 'rate_limit_exceeded',
  138. status_code: 429
  139. )
  140. # 添加限流信息
  141. response[:meta].merge!(limit_info) if limit_info.any?
  142. [response, 429]
  143. end
  144. # 分页响应
  145. # @param records [Array] 记录数组
  146. # @param pagination [Hash] 分页信息
  147. # @param message [String] 响应消息
  148. # @param additional_meta [Hash] 额外的元数据
  149. # @return [Hash] 标准化的分页响应格式
  150. def paginated_response(records:, pagination:, message: '获取成功', additional_meta: {})
  151. meta = standard_meta(pagination.merge(additional_meta))
  152. success_response(
  153. data: records,
  154. message: message,
  155. meta: meta
  156. )
  157. end
  158. # 创建成功响应
  159. # @param resource [Object] 创建的资源
  160. # @param resource_name [String] 资源名称
  161. # @return [Hash] 标准化的创建成功响应格式
  162. def create_success_response(resource, resource_name: '资源')
  163. message = "#{resource_name}创建成功"
  164. success_response(
  165. data: resource,
  166. message: message,
  167. status_code: 201
  168. )
  169. end
  170. # 更新成功响应
  171. # @param resource [Object] 更新的资源
  172. # @param resource_name [String] 资源名称
  173. # @return [Hash] 标准化的更新成功响应格式
  174. def update_success_response(resource, resource_name: '资源')
  175. message = "#{resource_name}更新成功"
  176. success_response(
  177. data: resource,
  178. message: message
  179. )
  180. end
  181. # 删除成功响应
  182. # @param resource_name [String] 资源名称
  183. # @return [Hash] 标准化的删除成功响应格式
  184. def destroy_success_response(resource_name: '资源')
  185. message = "#{resource_name}删除成功"
  186. success_response(
  187. data: nil,
  188. message: message
  189. )
  190. end
  191. # 批量操作响应
  192. # @param results [Hash] 批量操作结果
  193. # @param operation_name [String] 操作名称
  194. # @return [Hash] 标准化的批量操作响应格式
  195. def batch_operation_response(results, operation_name: '批量操作')
  196. successful_count = results[:successful]&.count || 0
  197. failed_count = results[:failed]&.count || 0
  198. total_count = results[:total] || successful_count + failed_count
  199. if failed_count == 0
  200. message = "#{operation_name}全部成功 (#{successful_count}/#{total_count})"
  201. elsif successful_count == 0
  202. message = "#{operation_name}全部失败 (0/#{total_count})"
  203. else
  204. message = "#{operation_name}部分成功 (#{successful_count}/#{total_count})"
  205. end
  206. success_response(
  207. data: results,
  208. message: message,
  209. meta: {
  210. successful_count: successful_count,
  211. failed_count: failed_count,
  212. total_count: total_count,
  213. success_rate: total_count > 0 ? (successful_count.to_f / total_count * 100).round(1) : 0
  214. }
  215. )
  216. end
  217. # 健康检查响应
  218. # @param additional_info [Hash] 额外的健康信息
  219. # @return [Hash] 健康检查响应格式
  220. def health_response(additional_info = {})
  221. health_data = {
  222. status: 'healthy',
  223. timestamp: Time.current.iso8601,
  224. environment: Rails.env,
  225. version: Rails.application.config.version || '1.0.0',
  226. uptime: number_to_human(Time.current - Rails.application.booted_at),
  227. memory_usage: number_to_human_size(`ps -o rss= -p #{Process.pid}`.to_i)
  228. }
  229. health_data.merge!(additional_info) if additional_info.any?
  230. success_response(
  231. data: health_data,
  232. message: '服务运行正常'
  233. )
  234. end
  235. private
  236. # 标准化元数据
  237. # @param meta [Hash] 原始元数据
  238. # @return [Hash] 标准化的元数据
  239. def standard_meta(meta = {})
  240. {
  241. version: (Rails.application.config.api_version rescue nil) || 'v1',
  242. server_time: Time.current.iso8601
  243. }.merge(meta)
  244. end
  245. # 添加请求ID到响应中
  246. # @param response [Hash] 响应对象
  247. def add_request_id(response)
  248. request_id = if defined?(RequestStore)
  249. RequestStore.store[:request_id]
  250. else
  251. Thread.current[:request_id]
  252. end
  253. response[:request_id] = request_id if request_id
  254. end
  255. end
  256. end

app/services/api_version_service.rb

0.0% lines covered

205 relevant lines. 0 lines covered and 205 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApiVersionService - API版本控制服务
  3. # 提供API版本管理、兼容性处理和版本信息查询
  4. class ApiVersionService
  5. # 当前支持的API版本
  6. SUPPORTED_VERSIONS = %w[v1].freeze
  7. # 默认API版本
  8. DEFAULT_VERSION = 'v1'.freeze
  9. # 版本弃用时间(天)
  10. DEPRECATION_DAYS = 90.freeze
  11. class << self
  12. # 从请求中确定API版本
  13. # @param request [ActionDispatch::Request] HTTP请求对象
  14. # @param available_versions [Array] 可用的版本列表
  15. # @return [String] 确定的API版本
  16. def determine_api_version(request, available_versions = SUPPORTED_VERSIONS)
  17. # 1. 从URL路径获取版本
  18. version_from_path = extract_version_from_path(request.path)
  19. return version_from_path if available_versions.include?(version_from_path)
  20. # 2. 从请求头获取版本
  21. version_from_header = request.headers['API-Version'] || request.headers['X-API-Version']
  22. return version_from_header if available_versions.include?(version_from_header)
  23. # 3. 从查询参数获取版本
  24. version_from_params = request.params['api_version'] || request.params['version']
  25. return version_from_params if available_versions.include?(version_from_params)
  26. # 4. 返回默认版本
  27. DEFAULT_VERSION
  28. end
  29. # 检查版本是否支持
  30. # @param version [String] 版本号
  31. # @param available_versions [Array] 可用的版本列表
  32. # @return [Boolean] 是否支持
  33. def version_supported?(version, available_versions = SUPPORTED_VERSIONS)
  34. available_versions.include?(version)
  35. end
  36. # 获取版本信息
  37. # @param version [String] 版本号
  38. # @return [Hash] 版本信息
  39. def version_info(version = DEFAULT_VERSION)
  40. version_configs = {
  41. 'v1' => {
  42. version: 'v1',
  43. name: 'QQClub API v1.0',
  44. description: 'QQClub读书会平台API的第一个稳定版本',
  45. release_date: '2025-10-15',
  46. status: 'stable',
  47. deprecated: false,
  48. sunset_date: nil,
  49. features: [
  50. '用户认证和授权',
  51. '共读活动管理',
  52. '打卡和进度跟踪',
  53. '小红花激励机制',
  54. '评论和互动系统',
  55. '通知系统',
  56. '内容搜索和导出',
  57. '数据统计分析'
  58. ],
  59. endpoints: {
  60. auth: [
  61. 'POST /api/auth/mock_login',
  62. 'POST /api/auth/wechat_login',
  63. 'POST /api/auth/refresh_token',
  64. 'GET /api/auth/me'
  65. ],
  66. events: [
  67. 'GET /api/v1/reading_events',
  68. 'POST /api/v1/reading_events',
  69. 'GET /api/v1/reading_events/:id',
  70. 'PUT /api/v1/reading_events/:id',
  71. 'DELETE /api/v1/reading_events/:id'
  72. ],
  73. check_ins: [
  74. 'POST /api/v1/check_ins',
  75. 'GET /api/v1/check_ins',
  76. 'GET /api/v1/check_ins/:id'
  77. ],
  78. flowers: [
  79. 'POST /api/v1/flowers',
  80. 'GET /api/v1/flowers',
  81. 'GET /api/v1/flower_leaderboards'
  82. ],
  83. notifications: [
  84. 'GET /api/v1/notifications',
  85. 'POST /api/v1/notifications/mark_all_read'
  86. ],
  87. analytics: [
  88. 'GET /api/v1/analytics/overview',
  89. 'GET /api/v1/analytics/dashboard'
  90. ]
  91. }
  92. }
  93. }
  94. version_configs[version] || {
  95. version: version,
  96. name: "Unknown Version",
  97. description: "版本信息未知",
  98. status: 'unknown',
  99. deprecated: false
  100. }
  101. end
  102. # 获取所有版本信息
  103. # @return [Array] 所有版本的详细信息
  104. def all_versions_info
  105. SUPPORTED_VERSIONS.map { |version| version_info(version) }
  106. end
  107. # 检查版本是否已弃用
  108. # @param version [String] 版本号
  109. # @return [Boolean] 是否已弃用
  110. def version_deprecated?(version)
  111. version_info(version)[:deprecated]
  112. end
  113. # 获取版本弃用信息
  114. # @param version [String] 版本号
  115. # @return [Hash] 弃用信息
  116. def deprecation_info(version)
  117. info = version_info(version)
  118. if info[:deprecated]
  119. {
  120. version: version,
  121. deprecated: true,
  122. sunset_date: info[:sunset_date],
  123. migration_guide: info[:migration_guide],
  124. alternative_versions: SUPPORTED_VERSIONS.reject { |v| v == version }
  125. }
  126. else
  127. {
  128. version: version,
  129. deprecated: false
  130. }
  131. end
  132. end
  133. # 创建版本响应头
  134. # @param version [String] 当前版本
  135. # @param response_headers [Hash] 响应头
  136. # @return [Hash] 更新后的响应头
  137. def create_version_headers(version, response_headers = {})
  138. headers = response_headers.dup
  139. # API版本信息
  140. headers['API-Version'] = version
  141. headers['Supported-Versions'] = SUPPORTED_VERSIONS.join(',')
  142. # 如果版本已弃用,添加弃用警告
  143. if version_deprecated?(version)
  144. headers['Deprecation'] = 'true'
  145. headers['Sunset'] = version_info(version)[:sunset_date] if version_info(version)[:sunset_date]
  146. headers['Migration-Guide'] = version_info(version)[:migration_guide] if version_info(version)[:migration_guide]
  147. end
  148. headers
  149. end
  150. # 生成版本弃用通知
  151. # @param version [String] 弃用的版本
  152. # @return [Hash] 弃用通知信息
  153. def generate_deprecation_warning(version)
  154. info = version_info(version)
  155. {
  156. warning: "API版本 #{version} 已弃用",
  157. message: "请升级到更新的API版本以获得更好的服务和功能",
  158. sunset_date: info[:sunset_date],
  159. days_until_sunset: info[:sunset_date] ? ((Date.parse(info[:sunset_date]) - Date.current).to_i) : nil,
  160. migration_guide: info[:migration_guide],
  161. recommended_version: DEFAULT_VERSION,
  162. supported_versions: SUPPORTED_VERSIONS.reject { |v| v == version }
  163. }
  164. end
  165. # 验证版本兼容性
  166. # @param requested_version [String] 请求的版本
  167. # @param available_versions [Array] 可用版本
  168. # @return [Hash] 兼容性检查结果
  169. def check_version_compatibility(requested_version, available_versions = SUPPORTED_VERSIONS)
  170. result = {
  171. requested_version: requested_version,
  172. compatible: false,
  173. supported: false,
  174. deprecated: false,
  175. recommended_version: DEFAULT_VERSION,
  176. messages: []
  177. }
  178. # 检查版本是否支持
  179. if version_supported?(requested_version, available_versions)
  180. result[:supported] = true
  181. result[:compatible] = true
  182. # 检查是否已弃用
  183. if version_deprecated?(requested_version)
  184. result[:deprecated] = true
  185. result[:messages] << "版本 #{requested_version} 已弃用,建议升级到 #{DEFAULT_VERSION}"
  186. deprecation_info = generate_deprecation_warning(requested_version)
  187. result[:deprecation_warning] = deprecation_info
  188. end
  189. else
  190. result[:messages] << "不支持的API版本: #{requested_version}"
  191. result[:messages] << "支持的版本: #{available_versions.join(', ')}"
  192. result[:messages] << "建议使用版本: #{DEFAULT_VERSION}"
  193. end
  194. result
  195. end
  196. # 从URL路径中提取版本号
  197. # @param path [String] URL路径
  198. # @return [String, nil] 版本号
  199. def extract_version_from_path(path)
  200. return nil unless path
  201. # 匹配 /api/v1/ 格式
  202. match = path.match(%r{/api/(v\d+)/})
  203. match ? match[1] : nil
  204. end
  205. # 获取版本变更日志
  206. # @param version [String] 版本号
  207. # @return [Array] 变更日志
  208. def changelog(version = DEFAULT_VERSION)
  209. changelog_data = {
  210. 'v1' => [
  211. {
  212. date: '2025-10-15',
  213. version: 'v1.0.0',
  214. type: 'release',
  215. description: 'QQClub API v1.0 正式发布',
  216. changes: [
  217. '实现完整的用户认证和授权系统',
  218. '提供共读活动管理功能',
  219. '支持打卡和进度跟踪',
  220. '引入小红花激励机制',
  221. '添加评论和互动系统',
  222. '实现通知系统',
  223. '提供内容搜索和导出功能',
  224. '集成数据统计分析'
  225. ],
  226. breaking_changes: [],
  227. new_features: [
  228. 'POST /api/v1/reading_events - 创建共读活动',
  229. 'POST /api/v1/check_ins - 提交打卡',
  230. 'POST /api/v1/flowers - 发送小红花',
  231. 'GET /api/v1/notifications - 获取通知列表',
  232. 'GET /api/v1/analytics/overview - 获取统计概览'
  233. ]
  234. }
  235. ]
  236. }
  237. changelog_data[version] || []
  238. end
  239. # 比较版本
  240. # @param version1 [String] 版本1
  241. # @param version2 [String] 版本2
  242. # @return [Integer] 比较结果 (-1, 0, 1)
  243. def compare_versions(version1, version2)
  244. v1_parts = version1.scan(/\d+/).map(&:to_i)
  245. v2_parts = version2.scan(/\d+/).map(&:to_i)
  246. max_length = [v1_parts.length, v2_parts.length].max
  247. max_length.times do |i|
  248. v1_part = v1_parts[i] || 0
  249. v2_part = v2_parts[i] || 0
  250. comparison = v1_part <=> v2_part
  251. return comparison unless comparison == 0
  252. end
  253. 0
  254. end
  255. # 获取最新的稳定版本
  256. # @return [String] 最新版本
  257. def latest_stable_version
  258. SUPPORTED_VERSIONS.select { |v| version_info(v)[:status] == 'stable' }
  259. .max { |a, b| compare_versions(a, b) }
  260. end
  261. end
  262. end

app/services/application_service.rb

62.5% lines covered

48 relevant lines. 30 lines covered and 18 lines missed.
    
  1. # frozen_string_literal: true
  2. # ApplicationService - 所有Service的基类
  3. # 提供统一的错误处理、成功/失败状态管理
  4. 1 class ApplicationService
  5. 1 include ActiveModel::Model
  6. 1 attr_reader :errors, :result
  7. 1 def initialize
  8. 3 @errors = []
  9. 3 @success = false
  10. 3 @result = nil
  11. end
  12. # 子类必须实现call方法
  13. 1 def call
  14. raise NotImplementedError, "子类必须实现call方法"
  15. end
  16. # 标记操作成功
  17. 1 def success!(result = nil)
  18. 4 @success = true
  19. 4 @result = result
  20. end
  21. # 标记操作失败
  22. 1 def failure!(error_messages)
  23. 1 @success = false
  24. 1 @errors = Array(error_messages)
  25. end
  26. # 检查操作是否成功
  27. 1 def success?
  28. @success
  29. end
  30. # 检查操作是否失败
  31. 1 def failure?
  32. !@success
  33. end
  34. # 添加错误信息
  35. 1 def add_error(message)
  36. @errors << message
  37. end
  38. # 检查是否有错误
  39. 1 def errors?
  40. @errors.any?
  41. end
  42. # 获取第一个错误信息
  43. 1 def first_error
  44. @errors.first
  45. end
  46. # 获取所有错误信息
  47. 1 def error_messages
  48. @errors
  49. end
  50. # 获取错误信息(别名)
  51. 1 def error_message
  52. @errors.first
  53. end
  54. # 清空错误信息
  55. 1 def clear_errors!
  56. @errors = []
  57. end
  58. 1 private
  59. # 块执行 - 统一异常处理
  60. 1 def handle_errors
  61. 3 yield
  62. rescue => e
  63. 1 Rails.logger.error "Service Error: #{e.message}"
  64. 1 Rails.logger.error e.backtrace.join("\n")
  65. 1 failure!("系统错误: #{e.message}")
  66. end
  67. # 验证必需参数
  68. 1 def require_params!(params, required_keys)
  69. missing_keys = required_keys.select { |key| params[key].blank? }
  70. if missing_keys.any?
  71. failure!("缺少必需参数: #{missing_keys.join(', ')}")
  72. return false
  73. end
  74. true
  75. end
  76. # 验证对象存在
  77. 1 def require_record!(record, error_message = "记录不存在")
  78. unless record
  79. failure!(error_message)
  80. return false
  81. end
  82. true
  83. end
  84. end

app/services/authentication_service.rb

0.0% lines covered

111 relevant lines. 0 lines covered and 111 lines missed.
    
  1. # frozen_string_literal: true
  2. # AuthenticationService - 用户认证逻辑服务
  3. # 负责微信登录、模拟登录、用户创建/查找等业务逻辑
  4. class AuthenticationService < ApplicationService
  5. attr_reader :login_params, :user, :login_type
  6. def initialize(login_params: {}, login_type: :mock)
  7. super()
  8. @login_params = login_params
  9. @login_type = login_type
  10. @user = nil
  11. end
  12. # 主要调用方法
  13. def call
  14. handle_errors do
  15. case login_type
  16. when :mock
  17. mock_login
  18. when :wechat
  19. wechat_login
  20. else
  21. failure!("不支持的登录类型: #{login_type}")
  22. end
  23. end
  24. self # 返回service实例
  25. end
  26. # 类方法:模拟登录
  27. def self.mock_login!(params = {})
  28. new(login_params: params, login_type: :mock).call
  29. end
  30. # 类方法:微信登录
  31. def self.wechat_login!(params = {})
  32. new(login_params: params, login_type: :wechat).call
  33. end
  34. private
  35. # 模拟登录逻辑
  36. def mock_login
  37. # 处理嵌套 JSON 参数或平铺参数
  38. # 优先使用顶级参数,如果没有则使用嵌套的user参数
  39. openid = login_params[:openid] || login_params.dig(:user, :wx_openid) || login_params.dig(:user, :openid) || "test_dhh_001"
  40. nickname = login_params[:nickname] || login_params.dig(:user, :nickname) || "DHH"
  41. avatar_url = login_params[:avatar_url] || login_params.dig(:user, :avatar_url)
  42. # 查找或创建用户
  43. @user = User.find_or_create_by(wx_openid: openid) do |u|
  44. u.nickname = nickname
  45. # 如果没有提供头像,生成一个随机头像
  46. u.avatar_url = avatar_url.presence || AvatarGeneratorService.generate_themed_avatar(
  47. nickname: nickname,
  48. user_id: openid
  49. )
  50. end
  51. # 如果用户已存在但没有头像,也生成一个
  52. if @user.avatar_url.blank? || @user.avatar_url.include?('example.com/avatar.jpg')
  53. @user.update!(
  54. avatar_url: AvatarGeneratorService.generate_themed_avatar(
  55. nickname: @user.nickname,
  56. user_id: @user.id
  57. )
  58. )
  59. end
  60. generate_token_response
  61. end
  62. # 微信登录逻辑
  63. def wechat_login
  64. code = login_params[:code]
  65. return failure!("缺少 code 参数") unless code
  66. # 获取小程序传递的用户信息
  67. user_info = login_params[:user_info] || {}
  68. openid = login_params[:openid] || user_info[:openid]
  69. unionid = login_params[:unionid] || user_info[:unionid]
  70. nickname = login_params[:nickname] || user_info[:nickname]
  71. avatar_url = login_params[:avatar_url] || login_params[:avatarUrl] || user_info[:avatar_url] || user_info[:avatarUrl]
  72. # 如果没有直接提供用户信息,尝试调用微信API获取
  73. if openid.blank? && code.present?
  74. wechat_result = fetch_wechat_openid(code)
  75. # 如果是测试环境且有用户信息,生成测试openid
  76. if wechat_result.nil? && user_info.present?
  77. openid = "test_wechat_#{Time.current.to_i}_#{rand(1000)}"
  78. Rails.logger.info "使用测试openid: #{openid} for user: #{user_info[:nickname]}"
  79. else
  80. return failure!("微信登录失败") unless wechat_result
  81. openid = wechat_result[:openid]
  82. unionid = wechat_result[:unionid]
  83. end
  84. end
  85. return failure!("无法获取用户标识") unless openid
  86. # 查找或创建用户
  87. @user = User.find_or_create_by(wx_openid: openid) do |u|
  88. u.wx_unionid = unionid if unionid.present?
  89. u.nickname = nickname.presence || "用户#{rand(1000..9999)}"
  90. # 优先使用微信提供的真实头像,如果没有则生成默认头像
  91. u.avatar_url = if avatar_url.present?
  92. avatar_url
  93. else
  94. AvatarGeneratorService.generate_themed_avatar(
  95. nickname: u.nickname,
  96. user_id: openid
  97. )
  98. end
  99. end
  100. # 如果用户已存在但头像为空或使用了默认头像,且当前提供了新头像,则更新
  101. if avatar_url.present? && (@user.avatar_url.blank? || @user.avatar_url.include?('example.com/avatar.jpg'))
  102. update_attrs = { avatar_url: avatar_url }
  103. update_attrs[:nickname] = nickname if nickname.present?
  104. @user.update!(update_attrs)
  105. end
  106. generate_token_response
  107. end
  108. # 生成Token响应
  109. def generate_token_response
  110. access_token = @user.generate_jwt_token
  111. refresh_token = @user.generate_refresh_token
  112. response_data = {
  113. access_token: access_token,
  114. refresh_token: refresh_token,
  115. user: user_data(@user)
  116. }
  117. success!(response_data)
  118. end
  119. # 格式化用户数据 - 返回字符串键格式用于API响应
  120. def user_data(user)
  121. {
  122. 'id' => user.id,
  123. 'nickname' => user.nickname,
  124. 'wx_openid' => user.wx_openid,
  125. 'avatar_url' => user.avatar_url,
  126. 'phone' => user.phone
  127. }
  128. end
  129. # 调用微信API获取openid(简化版本)
  130. def fetch_wechat_openid(code)
  131. # TODO: 配置 credentials 后实现
  132. # app_id = Rails.application.credentials.wechat[:app_id]
  133. # app_secret = Rails.application.credentials.wechat[:app_secret]
  134. # url = "https://api.weixin.qq.com/sns/jscode2session"
  135. # response = HTTParty.get(url, query: {
  136. # appid: app_id,
  137. # secret: app_secret,
  138. # js_code: code,
  139. # grant_type: "authorization_code"
  140. # })
  141. #
  142. # if response["openid"]
  143. # { openid: response["openid"], unionid: response["unionid"] }
  144. # else
  145. # nil
  146. # end
  147. # 暂时返回 nil,提示用户配置 credentials
  148. nil
  149. end
  150. end

app/services/avatar_generator_service.rb

0.0% lines covered

61 relevant lines. 0 lines covered and 61 lines missed.
    
  1. # frozen_string_literal: true
  2. # AvatarGeneratorService - 生成随机头像服务
  3. # 使用 Picsum Photos API 生成随机头像
  4. class AvatarGeneratorService < ApplicationService
  5. # 可用的头像主题和参数
  6. AVATAR_THEMES = %w[
  7. abstract animals architecture business cats city fashion
  8. food nature nightlife people sport technology transport
  9. ].freeze
  10. # 头像尺寸
  11. AVATAR_SIZES = [100, 200, 300, 400, 500].freeze
  12. # 生成用户头像URL
  13. def self.generate_user_avatar(user_id: nil, size: 200)
  14. # 基于用户ID生成一致的随机头像
  15. seed = user_id ? "user_#{user_id}" : "user_#{Time.current.to_i}_#{rand(1000)}"
  16. theme = AVATAR_THEMES.sample
  17. width = height = size
  18. # 使用 Picsum Photos API 生成随机头像
  19. "https://picsum.photos/seed/#{seed}/#{width}/#{height}.jpg"
  20. end
  21. # 生成动物头像(适合可爱风格)
  22. def self.generate_cute_avatar(user_id: nil)
  23. seed = user_id ? "cute_#{user_id}" : "cute_#{Time.current.to_i}_#{rand(1000)}"
  24. "https://picsum.photos/seed/#{seed}/200/200.jpg"
  25. end
  26. # 生成风景头像(适合通用风格)
  27. def self.generate_nature_avatar(user_id: nil)
  28. seed = user_id ? "nature_#{user_id}" : "nature_#{Time.current.to_i}_#{rand(1000)}"
  29. "https://picsum.photos/seed/#{seed}/200/200.jpg"
  30. end
  31. # 生成几何头像(适合现代风格)
  32. def self.generate_geometric_avatar(user_id: nil)
  33. seed = user_id ? "geo_#{user_id}" : "geo_#{Time.current.to_i}_#{rand(1000)}"
  34. "https://picsum.photos/seed/#{seed}/200/200.jpg"
  35. end
  36. # 根据用户昵称生成主题相关的头像
  37. def self.generate_themed_avatar(nickname: nil, user_id: nil)
  38. return generate_user_avatar(user_id: user_id) unless nickname
  39. # 根据昵称关键词选择主题
  40. theme_keywords = {
  41. '猫' => 'cats',
  42. '狗' => 'animals',
  43. '花' => 'nature',
  44. '树' => 'nature',
  45. '山' => 'nature',
  46. '海' => 'nature',
  47. '书' => 'business',
  48. '美食' => 'food',
  49. '运动' => 'sport',
  50. '音乐' => 'abstract',
  51. '科技' => 'technology',
  52. '城市' => 'city',
  53. '时尚' => 'fashion'
  54. }
  55. selected_theme = 'nature' # 默认主题
  56. theme_keywords.each do |keyword, theme|
  57. if nickname.include?(keyword)
  58. selected_theme = theme
  59. break
  60. end
  61. end
  62. seed = user_id ? "user_#{user_id}" : "user_#{Time.current.to_i}_#{rand(1000)}"
  63. "https://picsum.photos/seed/#{seed}_#{selected_theme}/200/200.jpg"
  64. end
  65. # 获取默认头像列表(用于测试)
  66. def self.default_avatars
  67. [
  68. 'https://picsum.photos/seed/avatar1/200/200.jpg',
  69. 'https://picsum.photos/seed/avatar2/200/200.jpg',
  70. 'https://picsum.photos/seed/avatar3/200/200.jpg',
  71. 'https://picsum.photos/seed/avatar4/200/200.jpg',
  72. 'https://picsum.photos/seed/avatar5/200/200.jpg'
  73. ]
  74. end
  75. end

app/services/cache_service.rb

0.0% lines covered

221 relevant lines. 0 lines covered and 221 lines missed.
    
  1. # frozen_string_literal: true
  2. # 缓存服务
  3. # 提供统一的缓存接口,支持多种缓存策略和数据类型
  4. class CacheService
  5. class << self
  6. # 缓存用户基本信息
  7. # @param user [User] 用户对象
  8. # @param ttl [Integer] 缓存时间(秒)
  9. # @return [Hash] 用户基本信息
  10. def cache_user_profile(user, ttl: 30.minutes)
  11. cache_key = "user_profile:#{user.id}"
  12. cached_data = Rails.cache.fetch(cache_key, expires_in: ttl) do
  13. {
  14. id: user.id,
  15. nickname: user.nickname,
  16. avatar_url: user.avatar_url,
  17. role: user.role_as_string,
  18. created_at: user.created_at,
  19. stats: {
  20. events_count: user.created_events.count,
  21. check_ins_count: user.check_ins.count,
  22. flowers_given: user.given_flowers.count,
  23. flowers_received: user.received_flowers.count
  24. }
  25. }
  26. end
  27. cached_data
  28. end
  29. # 缓存活动基本信息
  30. # @param event [ReadingEvent] 活动对象
  31. # @param ttl [Integer] 缓存时间(秒)
  32. # @return [Hash] 活动基本信息
  33. def cache_event_profile(event, ttl: 30.minutes)
  34. cache_key = "event_profile:#{event.id}"
  35. cached_data = Rails.cache.fetch(cache_key, expires_in: ttl) do
  36. {
  37. id: event.id,
  38. title: event.title,
  39. book_name: event.book_name,
  40. book_cover_url: event.book_cover_url,
  41. description: event.description&.truncate(200),
  42. status: event.status,
  43. approval_status: event.approval_status,
  44. start_date: event.start_date,
  45. end_date: event.end_date,
  46. max_participants: event.max_participants,
  47. leader: event.leader&.as_json_for_api,
  48. stats: {
  49. enrolled_count: event.event_enrollments.where(status: 'enrolled').count,
  50. check_ins_count: event.check_ins.count,
  51. flowers_count: event.flowers_count
  52. }
  53. }
  54. end
  55. cached_data
  56. end
  57. # 缓存排行榜数据
  58. # @param type [Symbol] 排行榜类型 (:flowers, :check_ins, :participation)
  59. # @param period [Symbol] 时间周期 (:today, :week, :month, :all_time)
  60. # @param limit [Integer] 返回记录数
  61. # @param ttl [Integer] 缓存时间(秒)
  62. # @return [Array] 排行榜数据
  63. def cache_leaderboard(type, period, limit: 10, ttl: 5.minutes)
  64. cache_key = "leaderboard:#{type}:#{period}:#{limit}"
  65. Rails.cache.fetch(cache_key, expires_in: ttl) do
  66. case type
  67. when :flowers
  68. AnalyticsService.leaderboards(:flowers, limit, period)
  69. when :check_ins
  70. AnalyticsService.leaderboards(:check_ins, limit, period)
  71. when :participation
  72. AnalyticsService.leaderboards(:participation, limit, period)
  73. else
  74. []
  75. end
  76. end
  77. end
  78. # 缓存用户统计信息
  79. # @param user [User] 用户对象
  80. # @param days [Integer] 统计天数
  81. # @param ttl [Integer] 缓存时间(秒)
  82. # @return [Hash] 用户统计信息
  83. def cache_user_analytics(user, days: 30, ttl: 10.minutes)
  84. cache_key = "user_analytics:#{user.id}:#{days}days"
  85. Rails.cache.fetch(cache_key, expires_in: ttl) do
  86. AnalyticsService.user_analytics(user, days)
  87. end
  88. end
  89. # 缓存系统统计信息
  90. # @param ttl [Integer] 缓存时间(秒)
  91. # @return [Hash] 系统统计信息
  92. def cache_system_overview(ttl: 1.hour)
  93. cache_key = "system_overview"
  94. Rails.cache.fetch(cache_key, expires_in: ttl) do
  95. AnalyticsService.system_overview
  96. end
  97. end
  98. # 缓存用户的未读通知数量
  99. # @param user [User] 用户对象
  100. # @param ttl [Integer] 缓存时间(秒)
  101. # @return [Integer] 未读通知数量
  102. def cache_unread_notifications_count(user, ttl: 1.minute)
  103. cache_key = "unread_notifications:#{user.id}"
  104. Rails.cache.fetch(cache_key, expires_in: ttl) do
  105. user.received_notifications.unread.count
  106. end
  107. end
  108. # 缓存用户的活动报名状态
  109. # @param user [User] 用户对象
  110. # @param event [ReadingEvent] 活动对象
  111. # @param ttl [Integer] 缓存时间(秒)
  112. # @return [Hash] 报名状态信息
  113. def cache_event_enrollment_status(user, event, ttl: 5.minutes)
  114. cache_key = "enrollment_status:#{user.id}:#{event.id}"
  115. Rails.cache.fetch(cache_key, expires_in: ttl) do
  116. enrollment = event.event_enrollments.find_by(user: user)
  117. {
  118. enrolled: enrollment.present?,
  119. status: enrollment&.status,
  120. enrollment_date: enrollment&.created_at,
  121. can_enroll: event.can_enroll?,
  122. is_full: event.full?,
  123. check_ins_count: enrollment&.check_ins_count || 0,
  124. completion_rate: enrollment&.completion_rate || 0
  125. }
  126. end
  127. end
  128. # 缓存今日小红花配额信息
  129. # @param user [User] 用户对象
  130. # @param event [ReadingEvent] 活动对象
  131. # @param ttl [Integer] 缓存时间(秒)
  132. # @return [Hash] 配额信息
  133. def cache_flower_quota_info(user, event, ttl: 1.minute)
  134. cache_key = "flower_quota:#{user.id}:#{event.id}:#{Date.current}"
  135. Rails.cache.fetch(cache_key, expires_in: ttl) do
  136. FlowerQuotaService.get_daily_quota(user, event, Date.current)
  137. end
  138. end
  139. # 批量缓存用户基本信息
  140. # @param users [Array<User>] 用户数组
  141. # @param ttl [Integer] 缓存时间(秒)
  142. # @return [Hash] 用户ID到缓存的映射
  143. def batch_cache_user_profiles(users, ttl: 30.minutes)
  144. return {} if users.empty?
  145. # 批量查找需要缓存的用户
  146. user_ids = users.map(&:id)
  147. existing_cache_keys = user_ids.map { |id| "user_profile:#{id}" }
  148. cached_data = Rails.cache.read_multi(*existing_cache_keys)
  149. # 找出需要重新缓存的用户
  150. uncached_users = users.reject { |user| cached_data.key?("user_profile:#{user.id}") }
  151. # 批量缓存未缓存的用户
  152. uncached_users.each do |user|
  153. cache_user_profile(user, ttl: ttl)
  154. end
  155. # 返回所有用户的缓存数据
  156. user_ids.index_with do |user_id|
  157. Rails.cache.read("user_profile:#{user_id}")
  158. end.to_h
  159. end
  160. # 缓存搜索结果
  161. # @param search_term [String] 搜索关键词
  162. # @param search_type [Symbol] 搜索类型
  163. # @param results [Array] 搜索结果
  164. # @param ttl [Integer] 缓存时间(秒)
  165. # @return [Array] 缓存的搜索结果
  166. def cache_search_results(search_term, search_type, results, ttl: 15.minutes)
  167. return results if search_term.blank? || results.empty?
  168. cache_key = "search:#{search_type}:#{Digest::MD5.hexdigest(search_term.downcase)}"
  169. Rails.cache.write(cache_key, results, expires_in: ttl)
  170. results
  171. end
  172. # 获取缓存的搜索结果
  173. # @param search_term [String] 搜索关键词
  174. # @param search_type [Symbol] 搜索类型
  175. # @return [Array, nil] 缓存的搜索结果或nil
  176. def get_cached_search_results(search_term, search_type)
  177. return nil if search_term.blank?
  178. cache_key = "search:#{search_type}:#{Digest::MD5.hexdigest(search_term.downcase)}"
  179. Rails.cache.read(cache_key)
  180. end
  181. # 缓存热门关键词
  182. # @param keywords [Array<String>] 关键词数组
  183. # @param ttl [Integer] 缓存时间(秒)
  184. # @return [Array<String>] 热门关键词
  185. def cache_popular_keywords(keywords, ttl: 1.hour)
  186. cache_key = "popular_keywords"
  187. Rails.cache.fetch(cache_key, expires_in: ttl) do
  188. keywords.first(10) # 只保留前10个
  189. end
  190. end
  191. # 缓存配置信息
  192. # @param ttl [Integer] 缓存时间(秒)
  193. # @return [Hash] 配置信息
  194. def cache_app_config(ttl: 1.hour)
  195. cache_key = "app_config"
  196. Rails.cache.fetch(cache_key, expires_in: ttl) do
  197. {
  198. max_flowers_per_check_in: 3,
  199. max_check_in_length: 2000,
  200. min_check_in_length: 50,
  201. default_event_duration: 30.days,
  202. max_event_participants: 100,
  203. flower_quota_daily: 3,
  204. notification_unread_limit: 50
  205. }
  206. end
  207. end
  208. # 清除用户相关的缓存
  209. # @param user [User] 用户对象
  210. def clear_user_cache(user)
  211. cache_patterns = [
  212. "user_profile:#{user.id}",
  213. "user_analytics:#{user.id}:*",
  214. "unread_notifications:#{user.id}",
  215. "enrollment_status:#{user.id}:*",
  216. "flower_quota:#{user.id}:*"
  217. ]
  218. cache_patterns.each do |pattern|
  219. if pattern.include?('*')
  220. Rails.cache.delete_matched(pattern)
  221. else
  222. Rails.cache.delete(pattern)
  223. end
  224. end
  225. end
  226. # 清除活动相关的缓存
  227. # @param event [ReadingEvent] 活动对象
  228. def clear_event_cache(event)
  229. cache_patterns = [
  230. "event_profile:#{event.id}",
  231. "enrollment_status:*:#{event.id}",
  232. "leaderboard:*:*:*" # 清除所有排行榜缓存
  233. ]
  234. cache_patterns.each do |pattern|
  235. if pattern.include?('*')
  236. Rails.cache.delete_matched(pattern)
  237. else
  238. Rails.cache.delete(pattern)
  239. end
  240. end
  241. end
  242. # 清除系统统计缓存
  243. def clear_system_cache
  244. Rails.cache.delete_matched("system_overview")
  245. Rails.cache.delete_matched("leaderboard:*")
  246. Rails.cache.delete_matched("popular_keywords")
  247. end
  248. # 预热缓存
  249. # 预加载常用数据到缓存中
  250. def warm_up_cache
  251. # 缓存系统概览
  252. cache_system_overview
  253. # 缓存热门排行榜
  254. [:flowers, :check_ins].each do |type|
  255. [:today, :week, :month].each do |period|
  256. cache_leaderboard(type, period)
  257. end
  258. end
  259. # 缓存应用配置
  260. cache_app_config
  261. Rails.logger.info "缓存预热完成"
  262. end
  263. # 获取缓存统计信息
  264. # @return [Hash] 缓存统计
  265. def cache_stats
  266. if Rails.cache.respond_to?(:stats)
  267. Rails.cache.stats
  268. else
  269. {
  270. cache_store: Rails.cache.class.name,
  271. message: "当前缓存存储不支持统计功能"
  272. }
  273. end
  274. end
  275. # 检查缓存健康状态
  276. # @return [Hash] 健康状态
  277. def cache_health_check
  278. test_key = "health_check_#{Time.current.to_i}"
  279. test_value = { test: true, timestamp: Time.current }
  280. begin
  281. # 写入测试
  282. Rails.cache.write(test_key, test_value, expires_in: 1.minute)
  283. # 读取测试
  284. cached_value = Rails.cache.read(test_key)
  285. # 清理测试数据
  286. Rails.cache.delete(test_key)
  287. {
  288. status: cached_value == test_value ? "healthy" : "unhealthy",
  289. cache_store: Rails.cache.class.name,
  290. test_time: Time.current
  291. }
  292. rescue => e
  293. {
  294. status: "error",
  295. cache_store: Rails.cache.class.name,
  296. error: e.message,
  297. test_time: Time.current
  298. }
  299. end
  300. end
  301. end
  302. end

app/services/concerns/service_interface.rb

0.0% lines covered

81 relevant lines. 0 lines covered and 81 lines missed.
    
  1. # frozen_string_literal: true
  2. # ServiceInterface - 服务接口规范模块
  3. # 提供统一的服务接口和数据处理方法
  4. module ServiceInterface
  5. extend ActiveSupport::Concern
  6. # 统一的数据访问方法
  7. def data
  8. @result
  9. end
  10. # 获取服务状态信息
  11. def status_info
  12. {
  13. success: success?,
  14. failure: failure?,
  15. errors: error_messages,
  16. has_errors: errors?,
  17. error_count: error_messages.count,
  18. first_error: first_error
  19. }
  20. end
  21. # 安全的数据获取(失败时返回默认值)
  22. def safe_data(default_value = nil)
  23. success? ? data : default_value
  24. end
  25. # 检查服务是否可用于当前用户
  26. def available_for_user?(user = nil)
  27. # 默认实现,子类可以重写
  28. true
  29. end
  30. # 获取服务类型标识
  31. def service_type
  32. self.class.name
  33. end
  34. # 获取服务描述
  35. def service_description
  36. self.class.name.demodulize.gsub(/Service$/, '')
  37. end
  38. # 批量操作结果格式化
  39. def format_batch_results(results, operation_name: '批量操作')
  40. successful_count = results.count { |r| r[:success] }
  41. failed_count = results.count - successful_count
  42. total_count = results.count
  43. {
  44. operation: operation_name,
  45. success: successful_count == total_count,
  46. summary: {
  47. total: total_count,
  48. successful: successful_count,
  49. failed: failed_count,
  50. success_rate: total_count > 0 ? (successful_count.to_f / total_count * 100).round(2) : 0
  51. },
  52. results: results
  53. }
  54. end
  55. protected
  56. # 验证用户权限
  57. def authorize_user!(user, required_permission = nil)
  58. return failure!("用户不能为空") unless user
  59. return failure!("用户不存在") unless user.persisted?
  60. if required_permission
  61. unless user.respond_to?(required_permission) && user.send(required_permission)
  62. return failure!("权限不足,需要权限: #{required_permission}")
  63. end
  64. end
  65. true
  66. end
  67. # 验证必需参数
  68. def validate_required_params(params, required_fields)
  69. missing_fields = required_fields.select { |field| params[field].blank? }
  70. if missing_fields.any?
  71. failure!("缺少必需参数: #{missing_fields.join(', ')}")
  72. return false
  73. end
  74. true
  75. end
  76. # 验证记录存在
  77. def validate_record_exists(record, name = '记录')
  78. unless record
  79. failure!("#{name}不存在")
  80. return false
  81. end
  82. unless record.persisted?
  83. failure!("#{name}未保存")
  84. return false
  85. end
  86. true
  87. end
  88. # 记录服务操作日志
  89. def log_service_action(action, additional_info = {})
  90. Rails.logger.info "Service #{service_type}: #{action} - #{additional_info}"
  91. end
  92. # 记录服务错误日志
  93. def log_service_error(error, additional_info = {})
  94. Rails.logger.error "Service #{service_type} Error: #{error.message}"
  95. Rails.logger.error additional_info if additional_info.any?
  96. end
  97. end

app/services/content_export_service.rb

0.0% lines covered

398 relevant lines. 0 lines covered and 398 lines missed.
    
  1. # 内容导出服务
  2. # 提供打卡内容的多格式导出功能
  3. class ContentExportService
  4. require 'prawn' # PDF生成
  5. require 'prawn/table' # PDF表格
  6. require 'csv' # CSV导出
  7. class ExportOptions
  8. attr_accessor :format, :check_in_ids, :user_id, :event_id, :date_from, :date_to,
  9. :include_metadata, :include_comments, :include_flowers,
  10. :sort_by, :sort_direction, :template
  11. def initialize(params = {})
  12. @format = params[:format] || 'pdf'
  13. @check_in_ids = params[:check_in_ids]&.split(',')&.map(&:to_i)
  14. @user_id = params[:user_id]&.to_i
  15. @event_id = params[:event_id]&.to_i
  16. @date_from = parse_date(params[:date_from])
  17. @date_to = parse_date(params[:date_to])
  18. @include_metadata = params[:include_metadata] != 'false'
  19. @include_comments = params[:include_comments] == 'true'
  20. @include_flowers = params[:include_flowers] == 'true'
  21. @sort_by = params[:sort_by] || 'created_at'
  22. @sort_direction = params[:sort_direction] || 'desc'
  23. @template = params[:template] || 'default'
  24. end
  25. def valid_format?
  26. %w[pdf markdown html txt csv].include?(format)
  27. end
  28. private
  29. def parse_date(date_string)
  30. return nil if date_string.blank?
  31. Date.parse(date_string)
  32. rescue ArgumentError, TypeError
  33. nil
  34. end
  35. end
  36. class ExportResult
  37. attr_accessor :content, :filename, :content_type, :size, :check_ins_count
  38. def initialize
  39. @content = ''
  40. @filename = ''
  41. @content_type = 'application/octet-stream'
  42. @size = 0
  43. @check_ins_count = 0
  44. end
  45. def success?
  46. !content.empty?
  47. end
  48. end
  49. class << self
  50. # 主要导出方法
  51. def export(params = {})
  52. options = ExportOptions.new(params)
  53. unless options.valid_format?
  54. result = ExportResult.new
  55. result.filename = "export_error.txt"
  56. result.content = "不支持的导出格式: #{options.format}"
  57. result.content_type = "text/plain"
  58. return result
  59. end
  60. # 获取要导出的打卡记录
  61. check_ins = get_check_ins_for_export(options)
  62. if check_ins.empty?
  63. result = ExportResult.new
  64. result.filename = "empty_export.#{options.format}"
  65. result.content = "没有找到符合条件的打卡记录"
  66. result.content_type = "text/plain"
  67. return result
  68. end
  69. # 根据格式执行导出
  70. result = case options.format
  71. when 'pdf'
  72. export_to_pdf(check_ins, options)
  73. when 'markdown'
  74. export_to_markdown(check_ins, options)
  75. when 'html'
  76. export_to_html(check_ins, options)
  77. when 'txt'
  78. export_to_text(check_ins, options)
  79. when 'csv'
  80. export_to_csv(check_ins, options)
  81. else
  82. export_to_text(check_ins, options)
  83. end
  84. result.check_ins_count = check_ins.count
  85. result
  86. rescue => e
  87. result = ExportResult.new
  88. result.filename = "export_error.txt"
  89. result.content = "导出过程中发生错误: #{e.message}"
  90. result.content_type = "text/plain"
  91. result
  92. end
  93. # 批量导出
  94. def batch_export(params_array = [])
  95. results = []
  96. params_array.each_with_index do |params, index|
  97. result = export(params)
  98. result.filename = "batch_export_#{index + 1}_#{result.filename}"
  99. results << result
  100. end
  101. results
  102. end
  103. # 获取导出统计信息
  104. def export_statistics(params = {})
  105. options = ExportOptions.new(params)
  106. check_ins = get_check_ins_for_export(options)
  107. {
  108. total_check_ins: check_ins.count,
  109. total_words: check_ins.sum(:word_count),
  110. date_range: {
  111. from: check_ins.minimum(:created_at)&.to_date,
  112. to: check_ins.maximum(:created_at)&.to_date
  113. },
  114. users_count: check_ins.distinct.count(:user_id),
  115. events_count: check_ins.joins(:reading_event).distinct.count(:reading_event_id),
  116. format_options: %w[pdf markdown html txt csv]
  117. }
  118. end
  119. private
  120. # 获取要导出的打卡记录
  121. def get_check_ins_for_export(options)
  122. query = CheckIn.includes(:user, :reading_schedule, :reading_event, :flowers, :comments)
  123. # 按ID筛选
  124. if options.check_in_ids.present?
  125. query = query.where(id: options.check_in_ids)
  126. end
  127. # 按用户筛选
  128. if options.user_id.present?
  129. query = query.where(user_id: options.user_id)
  130. end
  131. # 按活动筛选
  132. if options.event_id.present?
  133. query = query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: options.event_id })
  134. end
  135. # 按日期范围筛选
  136. if options.date_from.present?
  137. query = query.where('check_ins.created_at >= ?', options.date_from.beginning_of_day)
  138. end
  139. if options.date_to.present?
  140. query = query.where('check_ins.created_at <= ?', options.date_to.end_of_day)
  141. end
  142. # 排序
  143. case options.sort_by
  144. when 'created_at'
  145. query = query.order("created_at #{options.sort_direction.upcase}")
  146. when 'word_count'
  147. query = query.order("word_count #{options.sort_direction.upcase}")
  148. when 'flowers_count'
  149. query = query.left_joins(:flowers).group('check_ins.id').order("COUNT(flowers.id) #{options.sort_direction.upcase}")
  150. else
  151. query = order(created_at: :desc)
  152. end
  153. query
  154. end
  155. # 导出为PDF
  156. def export_to_pdf(check_ins, options)
  157. result = ExportResult.new
  158. # 创建PDF文档
  159. Prawn::Document.generate(StringIO.new) do |pdf|
  160. # 设置字体
  161. pdf.font_families.update(
  162. 'NotoSansCJK' => {
  163. normal: Rails.root.join('app', 'assets', 'fonts', 'NotoSansCJK-Regular.ttc'),
  164. bold: Rails.root.join('app', 'assets', 'fonts', 'NotoSansCJK-Bold.ttc')
  165. }
  166. )
  167. pdf.font 'NotoSansCJK'
  168. # 标题
  169. pdf.text '打卡内容导出', size: 24, style: :bold, align: :center
  170. pdf.move_down 20
  171. # 导出信息
  172. if options.include_metadata
  173. pdf.text "导出时间: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}", size: 10
  174. pdf.text "打卡数量: #{check_ins.count}", size: 10
  175. pdf.text "总字数: #{check_ins.sum(:word_count)}", size: 10
  176. pdf.move_down 20
  177. end
  178. # 打卡内容
  179. check_ins.each_with_index do |check_in, index|
  180. pdf.start_new_page if index > 0
  181. # 打卡标题
  182. pdf.text "打卡 ##{index + 1}", size: 16, style: :bold
  183. pdf.text "用户: #{check_in.user.nickname}", size: 12
  184. pdf.text "时间: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}", size: 12
  185. pdf.text "字数: #{check_in.word_count}", size: 12
  186. pdf.text "状态: #{check_in.status_text}", size: 12
  187. pdf.move_down 10
  188. # 打卡内容
  189. pdf.text "内容:", size: 14, style: :bold
  190. pdf.text check_in.content, size: 12
  191. pdf.move_down 10
  192. # 小红花
  193. if options.include_flowers && check_in.flowers.any?
  194. pdf.text "小红花:", size: 14, style: :bold
  195. check_in.flowers.each do |flower|
  196. pdf.text "- #{flower.giver.nickname}: #{flower.comment}", size: 10
  197. end
  198. pdf.move_down 10
  199. end
  200. # 评论
  201. if options.include_comments && check_in.comments.any?
  202. pdf.text "评论:", size: 14, style: :bold
  203. check_in.comments.each do |comment|
  204. pdf.text "- #{comment.user.nickname}: #{comment.content}", size: 10
  205. end
  206. end
  207. pdf.move_down 20
  208. end
  209. end.string
  210. result.content = pdf_content
  211. result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.pdf"
  212. result.content_type = 'application/pdf'
  213. result
  214. end
  215. # 导出为Markdown
  216. def export_to_markdown(check_ins, options)
  217. result = ExportResult.new
  218. content = []
  219. # Markdown头部
  220. content << "# 打卡内容导出"
  221. content << ""
  222. content << "**导出时间**: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}"
  223. content << "**打卡数量**: #{check_ins.count}"
  224. content << "**总字数**: #{check_ins.sum(:word_count)}"
  225. content << ""
  226. # 打卡内容
  227. check_ins.each_with_index do |check_in, index|
  228. content << "## 打卡 ##{index + 1}"
  229. content << ""
  230. content << "**用户**: #{check_in.user.nickname}"
  231. content << "**时间**: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}"
  232. content << "**字数**: #{check_in.word_count}"
  233. content << "**状态**: #{check_in.status_text}"
  234. content << ""
  235. content << "### 内容"
  236. content << ""
  237. content << check_in.content
  238. content << ""
  239. # 小红花
  240. if options.include_flowers && check_in.flowers.any?
  241. content << "### 小红花"
  242. content << ""
  243. check_in.flowers.each do |flower|
  244. content << "- **#{flower.giver.nickname}**: #{flower.comment}"
  245. end
  246. content << ""
  247. end
  248. # 评论
  249. if options.include_comments && check_in.comments.any?
  250. content << "### 评论"
  251. content << ""
  252. check_in.comments.each do |comment|
  253. content << "- **#{comment.user.nickname}**: #{comment.content}"
  254. end
  255. content << ""
  256. end
  257. content << "---"
  258. content << ""
  259. end
  260. result.content = content.join("\n")
  261. result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.md"
  262. result.content_type = 'text/markdown'
  263. result
  264. end
  265. # 导出为HTML
  266. def export_to_html(check_ins, options)
  267. result = ExportResult.new
  268. html = <<~HTML
  269. <!DOCTYPE html>
  270. <html lang="zh-CN">
  271. <head>
  272. <meta charset="UTF-8">
  273. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  274. <title>打卡内容导出</title>
  275. <style>
  276. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans CJK SC', sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
  277. .header { border-bottom: 2px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  278. .check-in { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
  279. .check-in-header { border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; }
  280. .user-info { color: #666; font-size: 14px; margin-bottom: 5px; }
  281. .content { margin: 15px 0; }
  282. .flowers, .comments { margin-top: 15px; padding: 10px; background: #f9f9f9; border-radius: 4px; }
  283. .flower-item, .comment-item { margin: 5px 0; font-size: 14px; }
  284. </style>
  285. </head>
  286. <body>
  287. <div class="header">
  288. <h1>打卡内容导出</h1>
  289. <p><strong>导出时间</strong>: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}</p>
  290. <p><strong>打卡数量</strong>: #{check_ins.count}</p>
  291. <p><strong>总字数</strong>: #{check_ins.sum(:word_count)}</p>
  292. </div>
  293. <div class="check-ins">
  294. HTML
  295. check_ins.each_with_index do |check_in, index|
  296. html += <<~HTML
  297. <div class="check-in">
  298. <div class="check-in-header">
  299. <h2>打卡 ##{index + 1}</h2>
  300. <div class="user-info">
  301. <span><strong>用户</strong>: #{check_in.user.nickname}</span> |
  302. <span><strong>时间</strong>: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}</span> |
  303. <span><strong>字数</strong>: #{check_in.word_count}</span> |
  304. <span><strong>状态</strong>: #{check_in.status_text}</span>
  305. </div>
  306. </div>
  307. <div class="content">
  308. <h3>内容</h3>
  309. <div>#{check_in.content.gsub("\n", "<br>")}</div>
  310. </div>
  311. HTML
  312. # 小红花
  313. if options.include_flowers && check_in.flowers.any?
  314. html += <<~HTML
  315. <div class="flowers">
  316. <h4>小红花</h4>
  317. #{check_in.flowers.map { |flower| "<div class=\"flower-item\"><strong>#{flower.giver.nickname}</strong>: #{flower.comment}</div>" }.join}
  318. </div>
  319. HTML
  320. end
  321. # 评论
  322. if options.include_comments && check_in.comments.any?
  323. html += <<~HTML
  324. <div class="comments">
  325. <h4>评论</h4>
  326. #{check_in.comments.map { |comment| "<div class=\"comment-item\"><strong>#{comment.user.nickname}</strong>: #{comment.content}</div>" }.join}
  327. </div>
  328. HTML
  329. end
  330. html += "</div>"
  331. end
  332. html += <<~HTML
  333. </div>
  334. </body>
  335. </html>
  336. HTML
  337. result.content = html
  338. result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.html"
  339. result.content_type = 'text/html'
  340. result
  341. end
  342. # 导出为纯文本
  343. def export_to_text(check_ins, options)
  344. result = ExportResult.new
  345. content = []
  346. # 文本头部
  347. content << "=" * 60
  348. content << "打卡内容导出"
  349. content << "=" * 60
  350. content << ""
  351. content << "导出时间: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}"
  352. content << "打卡数量: #{check_ins.count}"
  353. content << "总字数: #{check_ins.sum(:word_count)}"
  354. content << ""
  355. # 打卡内容
  356. check_ins.each_with_index do |check_in, index|
  357. content << "-" * 40
  358. content << "打卡 ##{index + 1}"
  359. content << "-" * 40
  360. content << "用户: #{check_in.user.nickname}"
  361. content << "时间: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}"
  362. content << "字数: #{check_in.word_count}"
  363. content << "状态: #{check_in.status_text}"
  364. content << ""
  365. content << "内容:"
  366. content << check_in.content
  367. content << ""
  368. # 小红花
  369. if options.include_flowers && check_in.flowers.any?
  370. content << "小红花:"
  371. check_in.flowers.each do |flower|
  372. content << "- #{flower.giver.nickname}: #{flower.comment}"
  373. end
  374. content << ""
  375. end
  376. # 评论
  377. if options.include_comments && check_in.comments.any?
  378. content << "评论:"
  379. check_in.comments.each do |comment|
  380. content << "- #{comment.user.nickname}: #{comment.content}"
  381. end
  382. content << ""
  383. end
  384. content << ""
  385. end
  386. result.content = content.join("\n")
  387. result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.txt"
  388. result.content_type = 'text/plain'
  389. result
  390. end
  391. # 导出为CSV
  392. def export_to_csv(check_ins, options)
  393. result = ExportResult.new
  394. CSV.generate(headers: true, write_headers: true) do |csv|
  395. headers = ['ID', '用户', '时间', '字数', '状态', '内容']
  396. headers += ['小红花数量'] if options.include_flowers
  397. headers += ['评论数量'] if options.include_comments
  398. csv << headers
  399. check_ins.each do |check_in|
  400. row = [
  401. check_in.id,
  402. check_in.user.nickname,
  403. check_in.created_at.strftime('%Y-%m-%d %H:%M:%S'),
  404. check_in.word_count,
  405. check_in.status_text,
  406. check_in.content.gsub("\n", " ")
  407. ]
  408. if options.include_flowers
  409. row << check_in.flowers.count
  410. end
  411. if options.include_comments
  412. row << check_in.comments.count
  413. end
  414. csv << row
  415. end
  416. end
  417. result.content = csv_content
  418. result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.csv"
  419. result.content_type = 'text/csv'
  420. result
  421. end
  422. end
  423. end
  424. # 扩展CheckIn模型以支持导出
  425. class CheckIn
  426. def status_text
  427. case status
  428. when 'normal'
  429. '正常打卡'
  430. when 'supplement'
  431. '补卡'
  432. when 'late'
  433. '迟到'
  434. else
  435. status.to_s
  436. end
  437. end
  438. end

app/services/content_formatter_service.rb

0.0% lines covered

273 relevant lines. 0 lines covered and 273 lines missed.
    
  1. # 内容格式化服务
  2. # 负责处理打卡内容的格式化、分段、表情转换等
  3. class ContentFormatterService
  4. include ActionView::Helpers::TextHelper
  5. include ActionView::Helpers::SanitizeHelper
  6. # 表情符号映射
  7. EMOJI_MAPPING = {
  8. '开心' => '😊',
  9. '快乐' => '😄',
  10. '哈哈' => '😂',
  11. '喜欢' => '❤️',
  12. '爱' => '💕',
  13. '赞' => '👍',
  14. '加油' => '💪',
  15. '思考' => '🤔',
  16. '学习' => '📚',
  17. '阅读' => '📖',
  18. '进步' => '📈',
  19. '努力' => '🌟',
  20. '感谢' => '🙏',
  21. '棒' => '👏',
  22. '好' => '👌',
  23. '支持' => '💯',
  24. '鼓励' => '🎉',
  25. '收获' => '🌱',
  26. '成长' => '🌿'
  27. }.freeze
  28. # 敏感词列表(简化版)
  29. SENSITIVE_WORDS = %w[
  30. 违法 暴力 色情 赌博 毒品
  31. # 实际应用中应该使用更完整的敏感词库
  32. ].freeze
  33. class << self
  34. # 格式化内容主体方法
  35. def format(content, options = {})
  36. formatted_content = content.dup
  37. # 应用各种格式化处理
  38. formatted_content = sanitize_content(formatted_content)
  39. formatted_content = convert_emojis(formatted_content)
  40. formatted_content = format_paragraphs(formatted_content)
  41. formatted_content = highlight_keywords(formatted_content, options[:keywords]) if options[:keywords].present?
  42. formatted_content = add_hashtag_links(formatted_content) if options[:enable_hashtags]
  43. formatted_content = truncate_content(formatted_content, options[:length]) if options[:length].present?
  44. formatted_content
  45. end
  46. # 生成内容摘要
  47. def generate_summary(content, max_length = 200)
  48. # 清理内容并生成摘要
  49. cleaned = sanitize_content(content)
  50. cleaned = remove_formatting(cleaned)
  51. if cleaned.length > max_length
  52. # 尝试在句号或换行符处截断
  53. truncated = cleaned.truncate(max_length, separator: /[,,.。!!??\n]/)
  54. truncated += "..." unless truncated.end_with?('.')
  55. truncated
  56. else
  57. cleaned
  58. end
  59. end
  60. # 提取关键词
  61. def extract_keywords(content, max_keywords = 5)
  62. cleaned = sanitize_content(content)
  63. # 简单的关键词提取逻辑(实际应用中可以使用更复杂的NLP算法)
  64. words = cleaned.scan(/[\u4e00-\u9fa5]+|[a-zA-Z]+/)
  65. .reject { |word| word.length < 2 }
  66. .group_by(&:itself)
  67. .transform_values(&:count)
  68. .sort_by { |_, count| -count }
  69. .first(max_keywords)
  70. .map(&:first)
  71. words
  72. end
  73. # 计算内容质量分数
  74. def calculate_quality_score(content)
  75. score = 0
  76. # 基础分数(长度要求)
  77. length = content.length
  78. if length >= 50
  79. score += 10
  80. elsif length >= 100
  81. score += 20
  82. elsif length >= 200
  83. score += 30
  84. end
  85. # 段落结构分数
  86. paragraphs = content.split(/\n\n+/).length
  87. score += [paragraphs * 2, 10].min
  88. # 关键词多样性分数
  89. keywords = extract_keywords(content, 10).length
  90. score += keywords * 2
  91. # 表情符号使用分数
  92. emoji_count = content.scan(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/).length
  93. score += [emoji_count, 5].min
  94. # 敏感词检测扣分
  95. sensitive_count = count_sensitive_words(content)
  96. score -= sensitive_count * 10
  97. [score, 0].max # 确保分数不为负
  98. end
  99. # 检查内容合规性
  100. def check_compliance(content)
  101. issues = []
  102. # 检查敏感词
  103. sensitive_words = find_sensitive_words(content)
  104. if sensitive_words.any?
  105. issues << {
  106. type: 'sensitive_words',
  107. message: "内容包含敏感词:#{sensitive_words.join(', ')}",
  108. severity: 'high'
  109. }
  110. end
  111. # 检查长度
  112. if content.length < 50
  113. issues << {
  114. type: 'too_short',
  115. message: "内容太短,至少需要50个字",
  116. severity: 'medium'
  117. }
  118. end
  119. # 检查是否为重复内容
  120. if is_duplicate_content?(content)
  121. issues << {
  122. type: 'duplicate',
  123. message: "内容疑似重复",
  124. severity: 'low'
  125. }
  126. end
  127. # 检查格式
  128. if content.match?(/^[^\n]*$/) # 没有换行
  129. issues << {
  130. type: 'poor_formatting',
  131. message: "建议分段以提高可读性",
  132. severity: 'low'
  133. }
  134. end
  135. {
  136. compliant: issues.empty?,
  137. issues: issues,
  138. score: calculate_quality_score(content)
  139. }
  140. end
  141. private
  142. # 清理内容,移除不安全的HTML
  143. def sanitize_content(content)
  144. # 简单的HTML清理实现
  145. cleaned = content.dup
  146. cleaned.gsub!(/<script[^>]*>.*?<\/script>/mi, '')
  147. cleaned.gsub!(/<style[^>]*>.*?<\/style>/mi, '')
  148. cleaned.gsub!(/<[^>]*>/, '')
  149. cleaned.strip
  150. end
  151. # 转换表情符号
  152. def convert_emojis(content)
  153. formatted = content.dup
  154. EMOJI_MAPPING.each do |text, emoji|
  155. formatted.gsub!(/#{text}/i, emoji)
  156. end
  157. formatted
  158. end
  159. # 格式化段落
  160. def format_paragraphs(content)
  161. # 将连续的换行符转换为段落
  162. paragraphs = content.split(/\n\n+/)
  163. formatted_paragraphs = paragraphs.map do |paragraph|
  164. # 处理单个段落内的换行
  165. lines = paragraph.split(/\n/)
  166. if lines.length == 1
  167. # 单行内容
  168. "<p>#{lines.first.strip}</p>"
  169. else
  170. # 多行内容,使用<br>连接
  171. "<p>#{lines.map(&:strip).join('<br>')}</p>"
  172. end
  173. end
  174. formatted_paragraphs.join("\n")
  175. end
  176. # 高亮关键词
  177. def highlight_keywords(content, keywords)
  178. formatted = content.dup
  179. Array(keywords).each do |keyword|
  180. next if keyword.blank?
  181. formatted.gsub!(/(#{Regexp.escape(keyword)})/i, '<mark>\1</mark>')
  182. end
  183. formatted
  184. end
  185. # 添加话题标签链接
  186. def add_hashtag_links(content)
  187. content.gsub(/#([^#\s]+)#?/) do |match|
  188. hashtag = $1
  189. "<a href='/search?q=%23#{hashtag}' class='hashtag'>##{hashtag}</a>"
  190. end
  191. end
  192. # 截断内容
  193. def truncate_content(content, length)
  194. # 简单的截断实现
  195. if content.length > length
  196. last_space = content.rindex(' ', length - 3)
  197. if last_space && last_space > 0
  198. content[0, last_space] + "..."
  199. else
  200. content[0, length - 3] + "..."
  201. end
  202. else
  203. content
  204. end
  205. end
  206. # 移除格式化标签
  207. def remove_formatting(content)
  208. # 移除所有HTML标签
  209. content.gsub(/<[^>]*>/, '').strip
  210. end
  211. # 统计敏感词数量
  212. def count_sensitive_words(content)
  213. count = 0
  214. SENSITIVE_WORDS.each do |word|
  215. count += content.scan(/#{word}/i).length
  216. end
  217. count
  218. end
  219. # 查找敏感词
  220. def find_sensitive_words(content)
  221. found_words = []
  222. SENSITIVE_WORDS.each do |word|
  223. if content.match?(/#{word}/i)
  224. found_words << word
  225. end
  226. end
  227. found_words
  228. end
  229. # 检查是否为重复内容(简化版)
  230. def is_duplicate_content?(content)
  231. # 这里可以实现更复杂的重复内容检测算法
  232. # 比如计算文本指纹、与历史记录对比等
  233. # 简单的重复检测:检查是否有大量重复字符
  234. max_consecutive_chars = content.scan(/(.)\1{5,}/).length
  235. return true if max_consecutive_chars > 0
  236. # 检查是否大部分内容都是标点符号
  237. punctuation_ratio = content.count('.,!?;:,。!?;:').to_f / content.length
  238. return true if punctuation_ratio > 0.3
  239. false
  240. end
  241. # 检查是否需要举报
  242. def should_report_content?(content, check_in = nil)
  243. compliance = check_compliance(content)
  244. # 包含敏感词的建议自动举报
  245. if compliance[:issues].any? { |issue| issue[:type] == 'sensitive_words' }
  246. return {
  247. should_report: true,
  248. reason: :sensitive_words,
  249. auto_report: true,
  250. detected_words: compliance[:issues].find { |i| i[:type] == 'sensitive_words' }&.dig(:detected_words) || []
  251. }
  252. end
  253. # 质量分数过低的建议举报
  254. if compliance[:score] < 20
  255. return {
  256. should_report: true,
  257. reason: :inappropriate_content,
  258. auto_report: false,
  259. quality_score: compliance[:score]
  260. }
  261. end
  262. { should_report: false }
  263. end
  264. # 生成举报建议
  265. def generate_report_suggestion(content, check_in = nil)
  266. analysis = should_report_content?(content, check_in)
  267. if analysis[:should_report]
  268. suggestion = case analysis[:reason]
  269. when :sensitive_words
  270. {
  271. reason: :sensitive_words,
  272. message: "内容包含敏感词:#{analysis[:detected_words].join(', ')}",
  273. auto_report: analysis[:auto_report],
  274. priority: 'high'
  275. }
  276. when :inappropriate_content
  277. {
  278. reason: :inappropriate_content,
  279. message: "内容质量过低,可能包含不当内容",
  280. auto_report: false,
  281. priority: 'medium'
  282. }
  283. else
  284. {
  285. reason: :other,
  286. message: "内容可能需要人工审核",
  287. auto_report: false,
  288. priority: 'low'
  289. }
  290. end
  291. else
  292. suggestion = {
  293. reason: nil,
  294. message: "内容正常,无需举报",
  295. auto_report: false,
  296. priority: 'low'
  297. }
  298. end
  299. suggestion.merge(
  300. compliance: check_compliance(content),
  301. sensitive_words: find_sensitive_words(content),
  302. quality_score: calculate_quality_score(content)
  303. )
  304. end
  305. # 检查用户举报权限
  306. def can_report_content?(user, check_in)
  307. # 不能举报自己的内容
  308. return false if user == check_in.user
  309. # 检查是否已经举报过
  310. existing_report = ContentReport.find_by(user: user, check_in: check_in)
  311. return false if existing_report
  312. true
  313. end
  314. # 预处理举报内容
  315. def preprocess_report_content(content)
  316. # 清理和预处理举报内容
  317. sanitized = sanitize_content(content)
  318. # 简单的截断实现
  319. if sanitized.length > 1000
  320. truncated = sanitized[0, 997] + "..."
  321. else
  322. truncated = sanitized
  323. end
  324. truncated.strip
  325. end
  326. end
  327. end

app/services/content_moderation_analytics_service.rb

0.0% lines covered

213 relevant lines. 0 lines covered and 213 lines missed.
    
  1. # frozen_string_literal: true
  2. # ContentModerationAnalyticsService - 内容审核统计分析服务
  3. # 专门负责举报数据的统计、分析和报告生成
  4. class ContentModerationAnalyticsService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :start_date, :end_date, :options
  7. def initialize(start_date: nil, end_date: nil, options: {})
  8. super()
  9. @start_date = start_date || 30.days.ago.to_date
  10. @end_date = end_date || Date.current
  11. @options = options.with_indifferent_access
  12. end
  13. # 获取综合统计报告
  14. def call
  15. handle_errors do
  16. validate_date_params
  17. generate_comprehensive_report
  18. end
  19. self
  20. end
  21. # 获取基本统计数据
  22. def self.get_basic_statistics(days = 30)
  23. new(
  24. start_date: days.days.ago.to_date,
  25. end_date: Date.current
  26. ).basic_statistics
  27. end
  28. # 生成审核报告
  29. def self.generate_moderation_report(start_date = nil, end_date = nil)
  30. new(
  31. start_date: start_date,
  32. end_date: end_date
  33. ).generate_moderation_report
  34. end
  35. private
  36. # 验证日期参数
  37. def validate_date_params
  38. return failure!("开始日期不能为空") unless start_date
  39. return failure!("结束日期不能为空") unless end_date
  40. return failure!("开始日期不能晚于结束日期") if start_date > end_date
  41. return failure!("时间范围不能超过一年") if (end_date - start_date).days > 365
  42. true
  43. end
  44. # 生成综合报告
  45. def generate_comprehensive_report
  46. reports = find_reports_in_period
  47. success!({
  48. period: {
  49. start: start_date,
  50. end: end_date,
  51. days_count: (end_date - start_date).to_i + 1
  52. },
  53. summary: generate_summary_statistics(reports),
  54. trends: generate_trend_analysis(reports),
  55. breakdown: generate_breakdown_analysis(reports),
  56. efficiency: generate_efficiency_metrics(reports),
  57. recommendations: generate_recommendations(reports)
  58. })
  59. end
  60. # 查找时间范围内的举报
  61. def find_reports_in_period
  62. ContentReport.where(
  63. created_at: start_date.beginning_of_day..end_date.end_of_day
  64. ).includes(:user, :admin, :target_content)
  65. end
  66. # 生成摘要统计
  67. def generate_summary_statistics(reports)
  68. {
  69. total_reports: reports.count,
  70. pending_reports: reports.pending.count,
  71. processed_reports: reports.where.not(status: :pending).count,
  72. auto_processed_reports: reports.where.not(admin_id: nil).where('reports.created_at = reports.updated_at').count,
  73. average_processing_time: calculate_average_processing_time(reports),
  74. reports_per_day: (reports.count.to_f / ((end_date - start_date).to_i + 1)).round(2)
  75. }
  76. end
  77. # 生成趋势分析
  78. def generate_trend_analysis(reports)
  79. {
  80. daily_trends: reports.group('DATE(created_at)').count,
  81. weekly_trends: generate_weekly_trends(reports),
  82. monthly_trends: generate_monthly_trends(reports),
  83. peak_hours: identify_peak_hours(reports),
  84. growth_rate: calculate_growth_rate(reports)
  85. }
  86. end
  87. # 生成周趋势
  88. def generate_weekly_trends(reports)
  89. reports.group("strftime('%Y-%W', created_at)").count
  90. end
  91. # 生成月趋势
  92. def generate_monthly_trends(reports)
  93. reports.group("strftime('%Y-%m', created_at)").count
  94. end
  95. # 识别举报高峰时段
  96. def identify_peak_hours(reports)
  97. reports.group("strftime('%H', created_at)").count.sort_by { |_, count| -count }.first(5)
  98. end
  99. # 计算增长率
  100. def calculate_growth_rate(reports)
  101. return {} if reports.count < 2
  102. first_half = reports.where(created_at: start_date..(start_date + ((end_date - start_date) / 2)))
  103. second_half = reports.where(created_at: ((start_date + ((end_date - start_date) / 2) + 1.day))..end_date)
  104. {
  105. first_half_count: first_half.count,
  106. second_half_count: second_half.count,
  107. growth_rate: calculate_percentage_change(first_half.count, second_half.count)
  108. }
  109. end
  110. # 生成分类分析
  111. def generate_breakdown_analysis(reports)
  112. {
  113. by_reason: reports.group(:reason).count,
  114. by_status: reports.group(:status).count,
  115. by_content_type: reports.joins(:target_content).group('target_contents.type').count,
  116. by_admin: reports.joins(:admin).where.not(admin_id: nil).group('users.nickname').count,
  117. by_reporter: reports.joins(:user).group('users.nickname').count.order('count DESC').limit(10),
  118. by_action_taken: reports.joins(:target_content).where(target_contents: { hidden: true }).count
  119. }
  120. end
  121. # 生成效率指标
  122. def generate_efficiency_metrics(reports)
  123. processed_reports = reports.where.not(status: :pending)
  124. {
  125. processing_rate: calculate_processing_rate(reports),
  126. average_resolution_time: calculate_average_processing_time(processed_reports),
  127. auto_processing_rate: calculate_auto_processing_rate(reports),
  128. admin_workload: calculate_admin_workload(reports),
  129. repeat_content_reports: calculate_repeat_content_reports(reports)
  130. }
  131. end
  132. # 计算处理率
  133. def calculate_processing_rate(reports)
  134. return 0 if reports.count == 0
  135. processed = reports.where.not(status: :pending).count
  136. (processed.to_f / reports.count * 100).round(2)
  137. end
  138. # 计算平均处理时间
  139. def calculate_average_processing_time(reports)
  140. return 0 if reports.empty?
  141. processed_reports = reports.where.not(status: :pending).where.not(updated_at: nil)
  142. return 0 if processed_reports.empty?
  143. total_time = processed_reports.sum do |report|
  144. (report.updated_at - report.created_at) / 1.hour # 转换为小时
  145. end
  146. (total_time / processed_reports.count).round(2)
  147. end
  148. # 计算自动处理率
  149. def calculate_auto_processing_rate(reports)
  150. return 0 if reports.count == 0
  151. auto_processed = reports.where.not(admin_id: nil)
  152. .where('reports.created_at = reports.updated_at')
  153. .count
  154. (auto_processed.to_f / reports.count * 100).round(2)
  155. end
  156. # 计算管理员工作量
  157. def calculate_admin_workload(reports)
  158. reports.joins(:admin)
  159. .where.not(admin_id: nil)
  160. .group('users.nickname')
  161. .count
  162. end
  163. # 计算重复内容举报
  164. def calculate_repeat_content_reports(reports)
  165. content_counts = reports.group(:target_content_id).count
  166. repeated_contents = content_counts.select { |_, count| count > 1 }
  167. {
  168. total_repeated_contents: repeated_contents.count,
  169. average_reports_per_content: repeated_contents.empty? ? 0 :
  170. (repeated_contents.values.sum.to_f / repeated_contents.count).round(2),
  171. most_reported_content: repeated_contents.max_by { |_, count| count }
  172. }
  173. end
  174. # 生成建议
  175. def generate_recommendations(reports)
  176. recommendations = []
  177. # 分析处理效率
  178. processing_rate = calculate_processing_rate(reports)
  179. if processing_rate < 80
  180. recommendations << {
  181. type: 'efficiency',
  182. priority: 'high',
  183. title: '提高举报处理效率',
  184. description: "当前处理率为#{processing_rate}%,建议优化审核流程或增加审核人员"
  185. }
  186. end
  187. # 分析自动处理效果
  188. auto_rate = calculate_auto_processing_rate(reports)
  189. if auto_rate < 30
  190. recommendations << {
  191. type: 'automation',
  192. priority: 'medium',
  193. title: '增加自动化处理',
  194. description: "当前自动处理率为#{auto_rate}%,建议增加敏感词检测等自动化规则"
  195. }
  196. end
  197. # 分析举报类型分布
  198. reason_breakdown = reports.group(:reason).count
  199. if reason_breakdown['sensitive_words']&.to_i&.>(reason_breakdown.values.sum * 0.4)
  200. recommendations << {
  201. type: 'prevention',
  202. priority: 'high',
  203. title: '加强敏感词预防',
  204. description: '敏感词举报占比较高,建议在内容发布时进行更好的预检查'
  205. }
  206. end
  207. recommendations
  208. end
  209. # 基本统计数据
  210. def basic_statistics
  211. reports = find_reports_in_period
  212. {
  213. total_reports: reports.count,
  214. pending_reports: reports.pending.count,
  215. processed_reports: reports.where.not(status: :pending).count,
  216. by_reason: reports.group(:reason).count,
  217. by_status: reports.group(:status).count
  218. }
  219. end
  220. # 生成审核报告
  221. def generate_moderation_report
  222. reports = find_reports_in_period
  223. {
  224. period: { start: start_date, end: end_date },
  225. summary: {
  226. total_reports: reports.count,
  227. pending_reports: reports.pending.count,
  228. processed_reports: reports.where.not(status: :pending).count,
  229. auto_processed_reports: reports.where.not(admin_id: nil).count
  230. },
  231. by_reason: reports.group(:reason).count,
  232. by_status: reports.group(:status).count,
  233. by_admin: reports.joins(:admin).group('users.nickname').count,
  234. daily_trends: reports.group('DATE(created_at)').count
  235. }
  236. end
  237. # 计算百分比变化
  238. def calculate_percentage_change(old_value, new_value)
  239. return 0 if old_value == 0
  240. (((new_value - old_value).to_f / old_value) * 100).round(2)
  241. end
  242. end

app/services/content_moderation_query_service.rb

0.0% lines covered

251 relevant lines. 0 lines covered and 251 lines missed.
    
  1. # frozen_string_literal: true
  2. # ContentModerationQueryService - 内容审核查询服务
  3. # 专门负责举报相关数据的查询和检索
  4. class ContentModerationQueryService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :current_user, :filters, :pagination_options
  7. def initialize(current_user: nil, filters: {}, pagination_options: {})
  8. super()
  9. @current_user = current_user
  10. @filters = filters.with_indifferent_access
  11. @pagination_options = pagination_options.with_indifferent_access
  12. end
  13. # 获取待处理的举报
  14. def call
  15. handle_errors do
  16. validate_query_permissions
  17. apply_filters_and_paginate
  18. format_query_results
  19. end
  20. self
  21. end
  22. # 类方法:获取待处理的举报
  23. def self.get_pending_reports(limit: 50, current_user: nil)
  24. new(
  25. current_user: current_user,
  26. filters: { status: 'pending' },
  27. pagination_options: { limit: limit }
  28. ).call
  29. end
  30. # 类方法:获取高优先级举报
  31. def self.get_high_priority_reports(current_user: nil)
  32. new(
  33. current_user: current_user,
  34. filters: { priority: 'high' }
  35. ).call
  36. end
  37. # 类方法:获取用户举报历史
  38. def self.get_user_report_history(user, limit: 20, current_user: nil)
  39. new(
  40. current_user: current_user,
  41. filters: { user_id: user.id },
  42. pagination_options: { limit: limit }
  43. ).call
  44. end
  45. # 类方法:获取被举报的内容
  46. def self.get_reported_content(limit: 50, status: nil, current_user: nil)
  47. query_filters = { limit: limit }
  48. query_filters[:status] = status if status.present?
  49. new(
  50. current_user: current_user,
  51. filters: query_filters
  52. ).get_reported_content_data
  53. end
  54. # 类方法:搜索举报
  55. def self.search_reports(query, current_user: nil)
  56. new(
  57. current_user: current_user,
  58. filters: { search: query }
  59. ).call
  60. end
  61. private
  62. # 验证查询权限
  63. def validate_query_permissions
  64. # 检查用户是否有权限查看举报数据
  65. if current_user && !current_user.can_approve_events? && !user_querying_own_reports?
  66. failure!("无权限查看举报数据")
  67. return false
  68. end
  69. true
  70. end
  71. # 检查是否查询自己的举报
  72. def user_querying_own_reports?
  73. filters[:user_id] == current_user&.id
  74. end
  75. # 应用过滤器和分页
  76. def apply_filters_and_paginate
  77. @reports = base_query
  78. # 应用各种过滤器
  79. apply_status_filter
  80. apply_reason_filter
  81. apply_user_filter
  82. apply_admin_filter
  83. apply_date_filter
  84. apply_priority_filter
  85. apply_search_filter
  86. # 应用排序
  87. apply_ordering
  88. # 应用分页
  89. apply_pagination
  90. true
  91. end
  92. # 基础查询
  93. def base_query
  94. ContentReport.includes(:user, :admin, :target_content)
  95. end
  96. # 应用状态过滤器
  97. def apply_status_filter
  98. return unless filters[:status].present?
  99. status_value = filters[:status]
  100. case status_value
  101. when 'pending'
  102. @reports = @reports.where(status: :pending)
  103. when 'processed'
  104. @reports = @reports.where.not(status: :pending)
  105. when 'approved'
  106. @reports = @reports.where(status: :approved)
  107. when 'rejected'
  108. @reports = @reports.where(status: :rejected)
  109. else
  110. @reports = @reports.where(status: status_value)
  111. end
  112. end
  113. # 应用原因过滤器
  114. def apply_reason_filter
  115. return unless filters[:reason].present?
  116. @reports = @reports.where(reason: filters[:reason])
  117. end
  118. # 应用用户过滤器
  119. def apply_user_filter
  120. return unless filters[:user_id].present?
  121. @reports = @reports.where(user_id: filters[:user_id])
  122. end
  123. # 应用管理员过滤器
  124. def apply_admin_filter
  125. return unless filters[:admin_id].present?
  126. @reports = @reports.where(admin_id: filters[:admin_id])
  127. end
  128. # 应用日期过滤器
  129. def apply_date_filter
  130. if filters[:start_date].present?
  131. start_date = Date.parse(filters[:start_date])
  132. @reports = @reports.where('created_at >= ?', start_date.beginning_of_day)
  133. end
  134. if filters[:end_date].present?
  135. end_date = Date.parse(filters[:end_date])
  136. @reports = @reports.where('created_at <= ?', end_date.end_of_day)
  137. end
  138. if filters[:days_ago].present?
  139. days_ago = filters[:days_ago].to_i
  140. @reports = @reports.where('created_at >= ?', days_ago.days.ago)
  141. end
  142. end
  143. # 应用优先级过滤器
  144. def apply_priority_filter
  145. return unless filters[:priority].present?
  146. case filters[:priority]
  147. when 'high'
  148. @reports = @reports.where(reason: %w[sensitive_words harassment])
  149. when 'medium'
  150. @reports = @reports.where(reason: %w[inappropriate_content spam])
  151. when 'low'
  152. @reports = @reports.where(reason: %w[other])
  153. end
  154. end
  155. # 应用搜索过滤器
  156. def apply_search_filter
  157. return unless filters[:search].present?
  158. search_term = "%#{filters[:search]}%"
  159. @reports = @reports.joins(:user, :target_content)
  160. .where(
  161. 'users.nickname ILIKE ? OR target_contents.content ILIKE ? OR content_reports.description ILIKE ?',
  162. search_term, search_term, search_term
  163. )
  164. end
  165. # 应用排序
  166. def apply_ordering
  167. sort_field = filters[:sort_by] || 'created_at'
  168. sort_direction = filters[:sort_direction] || 'desc'
  169. valid_fields = %w[created_at updated_at reason status user_id admin_id]
  170. if valid_fields.include?(sort_field)
  171. @reports = @reports.order(sort_field => sort_direction)
  172. else
  173. @reports = @reports.order(created_at: :desc)
  174. end
  175. end
  176. # 应用分页
  177. def apply_pagination
  178. limit = pagination_options[:limit] || 20
  179. page = pagination_options[:page] || 1
  180. offset = (page.to_i - 1) * limit.to_i
  181. @reports = @reports.limit(limit).offset(offset)
  182. # 记录分页信息用于响应
  183. @pagination_info = {
  184. current_page: page.to_i,
  185. per_page: limit.to_i,
  186. total_count: @reports.count,
  187. has_next_page: (@reports.count > (page.to_i * limit.to_i))
  188. }
  189. end
  190. # 格式化查询结果
  191. def format_query_results
  192. reports_data = @reports.map do |report|
  193. format_single_report(report)
  194. end
  195. response_data = {
  196. reports: reports_data
  197. }
  198. # 添加分页信息
  199. response_data[:pagination] = @pagination_info if @pagination_info
  200. # 添加统计信息
  201. response_data[:summary] = generate_query_summary if filters[:include_summary]
  202. success!(response_data)
  203. end
  204. # 格式化单个举报数据
  205. def format_single_report(report)
  206. {
  207. id: report.id,
  208. reason: report.reason,
  209. description: report.description,
  210. status: report.status,
  211. created_at: report.created_at,
  212. updated_at: report.updated_at,
  213. reporter: format_user_data(report.user),
  214. admin: format_user_data(report.admin),
  215. target_content: format_target_content(report.target_content),
  216. auto_processed: report.auto_processed?,
  217. processing_notes: report.notes
  218. }
  219. end
  220. # 格式化用户数据
  221. def format_user_data(user)
  222. return nil unless user
  223. {
  224. id: user.id,
  225. nickname: user.nickname,
  226. avatar_url: user.avatar_url,
  227. role: user.role_display_name
  228. }
  229. end
  230. # 格式化目标内容数据
  231. def format_target_content(content)
  232. return nil unless content
  233. {
  234. id: content.id,
  235. type: content.class.name,
  236. content_preview: content.respond_to?(:content) ? content.content.truncate(100) : '',
  237. user: format_user_data(content.user),
  238. created_at: content.created_at,
  239. hidden: content.respond_to?(:hidden?) ? content.hidden? : false
  240. }
  241. end
  242. # 生成查询摘要
  243. def generate_query_summary
  244. base_query = ContentReport.all
  245. apply_filters_to_summary_query(base_query)
  246. {
  247. total_reports: base_query.count,
  248. pending_reports: base_query.where(status: :pending).count,
  249. processed_reports: base_query.where.not(status: :pending).count,
  250. by_status: base_query.group(:status).count,
  251. by_reason: base_query.group(:reason).count
  252. }
  253. end
  254. # 对摘要查询应用过滤器
  255. def apply_filters_to_summary_query(query)
  256. # 这里复制上面的过滤器逻辑,但不需要分页和排序
  257. if filters[:status].present?
  258. query = query.where(status: filters[:status])
  259. end
  260. if filters[:reason].present?
  261. query = query.where(reason: filters[:reason])
  262. end
  263. if filters[:user_id].present?
  264. query = query.where(user_id: filters[:user_id])
  265. end
  266. query
  267. end
  268. # 获取被举报的内容数据
  269. def get_reported_content_data
  270. # 这是一个特殊查询,直接返回被举报的内容
  271. query = ContentReport.joins(:target_content)
  272. .includes(:target_content, :user)
  273. .distinct
  274. if filters[:status].present?
  275. query = query.where(content_reports: { status: filters[:status] })
  276. end
  277. contents = query.order('content_reports.created_at DESC')
  278. .limit(filters[:limit] || 50)
  279. {
  280. reported_contents: contents.map do |report|
  281. {
  282. content: format_target_content(report.target_content),
  283. reports_count: report.target_content.content_reports.count,
  284. latest_report: format_single_report(report)
  285. }
  286. end
  287. }
  288. end
  289. end

app/services/content_moderation_service.rb

0.0% lines covered

202 relevant lines. 0 lines covered and 202 lines missed.
    
  1. # frozen_string_literal: true
  2. # ContentModerationService - 内容审核服务(重构版)
  3. # 作为内容审核相关服务的协调器,提供统一的接口
  4. class ContentModerationService < ApplicationService
  5. attr_reader :action, :params, :current_user
  6. def initialize(action:, params: {}, current_user: nil)
  7. super()
  8. @action = action
  9. @params = params.with_indifferent_access
  10. @current_user = current_user
  11. end
  12. # 主要调用方法
  13. def call
  14. handle_errors do
  15. validate_action
  16. execute_action
  17. end
  18. self
  19. end
  20. # 类方法:创建举报
  21. def self.create_report(user, target_content, reason:, description: nil)
  22. new(
  23. action: :create_report,
  24. params: {
  25. user: user,
  26. target_content: target_content,
  27. reason: reason,
  28. description: description
  29. }
  30. ).call
  31. end
  32. # 类方法:批量处理举报
  33. def self.batch_process_reports(admin, report_ids, action:, notes: nil)
  34. new(
  35. action: :batch_process,
  36. params: {
  37. admin: admin,
  38. report_ids: report_ids,
  39. action: action,
  40. notes: notes
  41. }
  42. ).call
  43. end
  44. # 类方法:获取举报统计
  45. def self.get_statistics(days = 30)
  46. new(
  47. action: :get_statistics,
  48. params: { days: days }
  49. ).call
  50. end
  51. # 类方法:获取待处理的举报
  52. def self.get_pending_reports(limit: 50, current_user: nil)
  53. new(
  54. action: :get_pending_reports,
  55. params: { limit: limit },
  56. current_user: current_user
  57. ).call
  58. end
  59. # 类方法:获取高优先级举报
  60. def self.get_high_priority_reports(current_user: nil)
  61. new(
  62. action: :get_high_priority_reports,
  63. current_user: current_user
  64. ).call
  65. end
  66. # 类方法:生成审核报告
  67. def self.generate_moderation_report(start_date = nil, end_date = nil)
  68. new(
  69. action: :generate_report,
  70. params: {
  71. start_date: start_date,
  72. end_date: end_date
  73. }
  74. ).call
  75. end
  76. # 类方法:获取用户举报历史
  77. def self.get_user_report_history(user, limit: 20, current_user: nil)
  78. new(
  79. action: :get_user_history,
  80. params: {
  81. user: user,
  82. limit: limit
  83. },
  84. current_user: current_user
  85. ).call
  86. end
  87. # 类方法:获取被举报的内容
  88. def self.get_reported_content(limit: 50, status: nil, current_user: nil)
  89. new(
  90. action: :get_reported_content,
  91. params: {
  92. limit: limit,
  93. status: status
  94. },
  95. current_user: current_user
  96. ).call
  97. end
  98. # 类方法:检查内容是否需要自动审核
  99. def self.check_content_for_review(content)
  100. new(
  101. action: :check_content,
  102. params: { content: content }
  103. ).call
  104. end
  105. # 类方法:搜索举报
  106. def self.search_reports(query, current_user: nil)
  107. ContentModerationQueryService.search_reports(query, current_user: current_user)
  108. end
  109. private
  110. # 验证操作
  111. def validate_action
  112. valid_actions = [
  113. :create_report, :batch_process, :get_statistics, :get_pending_reports,
  114. :get_high_priority_reports, :generate_report, :get_user_history,
  115. :get_reported_content, :check_content
  116. ]
  117. unless valid_actions.include?(action)
  118. failure!("不支持的操作: #{action}")
  119. return false
  120. end
  121. true
  122. end
  123. # 执行具体操作
  124. def execute_action
  125. result = case action
  126. when :create_report
  127. create_report_action
  128. when :batch_process
  129. batch_process_action
  130. when :get_statistics
  131. get_statistics_action
  132. when :get_pending_reports
  133. get_pending_reports_action
  134. when :get_high_priority_reports
  135. get_high_priority_reports_action
  136. when :generate_report
  137. generate_report_action
  138. when :get_user_history
  139. get_user_history_action
  140. when :get_reported_content
  141. get_reported_content_action
  142. when :check_content
  143. check_content_action
  144. end
  145. if result&.success?
  146. success!(result.data)
  147. else
  148. failure!(result&.error_messages || ["操作失败"])
  149. end
  150. end
  151. # 创建举报操作
  152. def create_report_action
  153. ReportCreationService.new(
  154. user: params[:user],
  155. target_content: params[:target_content],
  156. reason: params[:reason],
  157. description: params[:description]
  158. ).call
  159. end
  160. # 批量处理操作
  161. def batch_process_action
  162. ReportProcessingService.new(
  163. admin: params[:admin],
  164. report_ids: params[:report_ids],
  165. action: params[:action],
  166. notes: params[:notes]
  167. ).call
  168. end
  169. # 获取统计操作
  170. def get_statistics_action
  171. days = params[:days] || 30
  172. service = ContentModerationAnalyticsService.new(
  173. start_date: days.days.ago.to_date,
  174. end_date: Date.current
  175. )
  176. service.call
  177. service
  178. end
  179. # 获取待处理举报操作
  180. def get_pending_reports_action
  181. limit = params[:limit] || 50
  182. ContentModerationQueryService.get_pending_reports(
  183. limit: limit,
  184. current_user: current_user
  185. )
  186. end
  187. # 获取高优先级举报操作
  188. def get_high_priority_reports_action
  189. ContentModerationQueryService.get_high_priority_reports(
  190. current_user: current_user
  191. )
  192. end
  193. # 生成报告操作
  194. def generate_report_action
  195. ContentModerationAnalyticsService.generate_moderation_report(
  196. params[:start_date],
  197. params[:end_date]
  198. )
  199. end
  200. # 获取用户历史操作
  201. def get_user_history_action
  202. limit = params[:limit] || 20
  203. ContentModerationQueryService.get_user_report_history(
  204. params[:user],
  205. limit: limit,
  206. current_user: current_user
  207. )
  208. end
  209. # 获取被举报内容操作
  210. def get_reported_content_action
  211. ContentModerationQueryService.get_reported_content(
  212. limit: params[:limit],
  213. status: params[:status],
  214. current_user: current_user
  215. )
  216. end
  217. # 检查内容操作
  218. def check_content_action
  219. content = params[:content]
  220. return failure!("内容不能为空") unless content
  221. # 这里应该调用内容检查服务
  222. # 为了简化,返回一个基本的检查结果
  223. {
  224. needs_review: false,
  225. priority: 'low',
  226. issues: []
  227. }
  228. end
  229. end

app/services/content_search_service.rb

0.0% lines covered

252 relevant lines. 0 lines covered and 252 lines missed.
    
  1. # 内容搜索服务
  2. # 提供打卡内容的全文搜索、高级搜索和推荐功能
  3. class ContentSearchService
  4. include ActionView::Helpers::SanitizeHelper
  5. class SearchOptions
  6. attr_accessor :query, :event_id, :user_id, :date_from, :date_to, :status,
  7. :quality_min, :quality_max, :keywords, :sort_by, :sort_direction,
  8. :page, :per_page
  9. def initialize(params = {})
  10. @query = params[:query]&.strip
  11. @event_id = params[:event_id]
  12. @user_id = params[:user_id]
  13. @date_from = parse_date(params[:date_from])
  14. @date_to = parse_date(params[:date_to])
  15. @status = params[:status]
  16. @quality_min = params[:quality_min]&.to_i
  17. @quality_max = params[:quality_max]&.to_i
  18. @keywords = params[:keywords]&.split(',')&.map(&:strip)
  19. @sort_by = params[:sort_by] || 'relevance'
  20. @sort_direction = params[:sort_direction] || 'desc'
  21. @page = params[:page]&.to_i || 1
  22. @per_page = params[:per_page]&.to_i || 20
  23. end
  24. private
  25. def parse_date(date_string)
  26. return nil if date_string.blank?
  27. Date.parse(date_string)
  28. rescue ArgumentError, TypeError
  29. nil
  30. end
  31. end
  32. class SearchResult
  33. attr_accessor :check_ins, :total_count, :total_pages, :current_page,
  34. :suggestions, :search_time, :facets
  35. def initialize
  36. @check_ins = []
  37. @total_count = 0
  38. @total_pages = 0
  39. @current_page = 1
  40. @suggestions = []
  41. @search_time = 0
  42. @facets = {}
  43. end
  44. def to_h
  45. {
  46. check_ins: check_ins.map(&:to_search_result_h),
  47. pagination: {
  48. current_page: current_page,
  49. total_pages: total_pages,
  50. total_count: total_count,
  51. per_page: search_options&.per_page || 20
  52. },
  53. suggestions: suggestions,
  54. search_time: search_time,
  55. facets: facets
  56. }
  57. end
  58. def search_options=(options)
  59. @search_options = options
  60. end
  61. attr_reader :search_options
  62. end
  63. class << self
  64. # 主要搜索方法
  65. def search(params = {})
  66. options = SearchOptions.new(params)
  67. result = SearchResult.new
  68. result.search_options = options
  69. start_time = Time.current
  70. # 执行搜索
  71. check_ins = perform_search(options)
  72. # 统计总数
  73. total_count = count_search_results(options)
  74. # 分页
  75. paginated_check_ins = check_ins.includes(:user, :reading_schedule, :flowers)
  76. .limit(options.per_page)
  77. .offset((options.page - 1) * options.per_page)
  78. # 计算搜索建议
  79. suggestions = generate_suggestions(options)
  80. # 生成搜索统计
  81. facets = generate_facets(check_ins)
  82. end_time = Time.current
  83. result.check_ins = paginated_check_ins.to_a
  84. result.total_count = total_count
  85. result.total_pages = (total_count.to_f / options.per_page).ceil
  86. result.current_page = options.page
  87. result.suggestions = suggestions
  88. result.search_time = ((end_time - start_time) * 1000).round(2)
  89. result.facets = facets
  90. result
  91. end
  92. # 高级搜索
  93. def advanced_search(params = {})
  94. # 高级搜索支持更复杂的条件组合
  95. options = SearchOptions.new(params)
  96. # 构建复杂查询
  97. check_ins = build_advanced_query(options)
  98. # 应用排序
  99. check_ins = apply_sorting(check_ins, options)
  100. {
  101. check_ins: check_ins.includes(:user, :reading_schedule),
  102. options: options
  103. }
  104. end
  105. # 推荐相关内容
  106. def recommend_related(check_in, limit = 5)
  107. # 基于内容相似性推荐相关打卡
  108. keywords = check_in.keywords(10)
  109. related_check_ins = CheckIn.joins(:reading_schedule)
  110. .where.not(id: check_in.id)
  111. .where(reading_schedules: { reading_event_id: check_in.reading_event_id })
  112. # 基于关键词匹配
  113. if keywords.any?
  114. keyword_conditions = keywords.map { |keyword| "check_ins.content LIKE ?" }.join(' OR ')
  115. keyword_values = keywords.map { |keyword| "%#{keyword}%" }
  116. related_check_ins = related_check_ins.where(keyword_conditions, *keyword_values)
  117. end
  118. # 按质量和时间排序
  119. related_check_ins.order('created_at DESC').limit(limit)
  120. end
  121. # 热门关键词
  122. def popular_keywords(limit = 20, days = 30)
  123. start_date = days.days.ago.to_date
  124. # 简化的关键词统计(实际应用中可以使用更复杂的算法)
  125. recent_check_ins = CheckIn.where('created_at >= ?', start_date)
  126. keyword_counts = Hash.new(0)
  127. recent_check_ins.find_each do |check_in|
  128. check_in.keywords(5).each do |keyword|
  129. keyword_counts[keyword] += 1
  130. end
  131. end
  132. keyword_counts.sort_by { |_, count| -count }.first(limit).to_h
  133. end
  134. # 搜索趋势
  135. def search_trends(days = 7)
  136. start_date = days.days.ago.to_date
  137. daily_stats = CheckIn.where('created_at >= ?', start_date)
  138. .group('DATE(created_at)')
  139. .count
  140. (0...days).map do |i|
  141. date = (Date.today - days + 1 + i)
  142. {
  143. date: date,
  144. count: daily_stats[date] || 0
  145. }
  146. end
  147. end
  148. private
  149. # 执行基础搜索
  150. def perform_search(options)
  151. query = CheckIn.joins(:user, :reading_schedule)
  152. # 文本搜索
  153. if options.query.present?
  154. query = apply_text_search(query, options.query)
  155. end
  156. # 活动筛选
  157. if options.event_id.present?
  158. query = query.where(reading_schedules: { reading_event_id: options.event_id })
  159. end
  160. # 用户筛选
  161. if options.user_id.present?
  162. query = query.where(user_id: options.user_id)
  163. end
  164. # 日期范围筛选
  165. if options.date_from.present?
  166. query = query.where('check_ins.created_at >= ?', options.date_from.beginning_of_day)
  167. end
  168. if options.date_to.present?
  169. query = query.where('check_ins.created_at <= ?', options.date_to.end_of_day)
  170. end
  171. # 状态筛选
  172. if options.status.present?
  173. query = query.where(status: options.status)
  174. end
  175. # 质量分数筛选
  176. if options.quality_min.present?
  177. # 这里需要添加quality_score字段的计算逻辑
  178. # 暂时使用简化版本
  179. query = query.where('word_count >= ?', options.quality_min * 10)
  180. end
  181. if options.quality_max.present?
  182. query = query.where('word_count <= ?', options.quality_max * 10)
  183. end
  184. # 关键词筛选
  185. if options.keywords.present?
  186. keyword_conditions = options.keywords.map { |keyword| "check_ins.content LIKE ?" }.join(' OR ')
  187. keyword_values = options.keywords.map { |keyword| "%#{keyword}%" }
  188. query = query.where(keyword_conditions, *keyword_values)
  189. end
  190. query
  191. end
  192. # 应用文本搜索
  193. def apply_text_search(query, search_query)
  194. # 简单的全文搜索实现
  195. # 实际应用中可以使用PostgreSQL的全文搜索或Elasticsearch
  196. search_terms = search_query.split(/\s+/).reject(&:blank?)
  197. search_terms.each do |term|
  198. query = query.where('check_ins.content LIKE ?', "%#{term}%")
  199. end
  200. query
  201. end
  202. # 统计搜索结果数量
  203. def count_search_results(options)
  204. perform_search(options).count
  205. end
  206. # 应用排序
  207. def apply_sorting(query, options)
  208. case options.sort_by
  209. when 'relevance'
  210. # 相关性排序(简化版)
  211. query.order('created_at DESC')
  212. when 'created_at'
  213. direction = options.sort_direction.upcase == 'ASC' ? 'ASC' : 'DESC'
  214. query.order("created_at #{direction}")
  215. when 'word_count'
  216. direction = options.sort_direction.upcase == 'ASC' ? 'ASC' : 'DESC'
  217. query.order("word_count #{direction}")
  218. when 'flowers_count'
  219. query = query.left_joins(:flowers)
  220. .group('check_ins.id')
  221. .order("COUNT(flowers.id) #{options.sort_direction.upcase}")
  222. else
  223. query.order('created_at DESC')
  224. end
  225. end
  226. # 生成搜索建议
  227. def generate_suggestions(options)
  228. suggestions = []
  229. # 如果没有结果,提供拼写建议
  230. if options.query.present? && options.query.length > 2
  231. # 简化的拼写检查
  232. suggestions << "尝试使用更简短的关键词"
  233. suggestions << "检查是否有拼写错误"
  234. end
  235. # 日期范围建议
  236. if options.date_from.blank? || options.date_to.blank?
  237. suggestions << "添加日期范围以缩小搜索结果"
  238. end
  239. # 关键词建议
  240. popular_keywords = popular_keywords(5)
  241. if popular_keywords.any?
  242. suggestions << "热门关键词:#{popular_keywords.keys.first(3).join(', ')}"
  243. end
  244. suggestions
  245. end
  246. # 生成搜索统计
  247. def generate_facets(check_ins)
  248. facets = {}
  249. # 按状态统计
  250. status_facet = check_ins.group(:status).count
  251. facets[:status] = status_facet.transform_keys { |status| status.to_s }
  252. # 按日期统计
  253. date_facet = check_ins.group('DATE(created_at)').count
  254. facets[:dates] = date_facet
  255. # 按用户统计(前10名)
  256. user_facet = check_ins.joins(:user).group('users.nickname').count
  257. .sort_by { |_, count| -count }.first(10).to_h
  258. facets[:users] = user_facet
  259. facets
  260. end
  261. # 构建高级查询
  262. def build_advanced_query(options)
  263. query = CheckIn.joins(:user, :reading_schedule)
  264. # 实现更复杂的查询逻辑
  265. # 例如:OR条件、NOT条件、短语搜索等
  266. query
  267. end
  268. end
  269. end
  270. # 扩展CheckIn模型以支持搜索结果格式化
  271. class CheckIn
  272. def to_search_result_h
  273. {
  274. id: id,
  275. content_preview: content_preview(150),
  276. formatted_content: formatted_content(length: 150),
  277. user: {
  278. id: user.id,
  279. nickname: user.nickname,
  280. avatar_url: user.avatar_url
  281. },
  282. reading_event: {
  283. id: reading_event.id,
  284. title: reading_event.title
  285. },
  286. reading_schedule: {
  287. id: reading_schedule.id,
  288. date: reading_schedule.date,
  289. day_number: reading_schedule.day_number
  290. },
  291. word_count: word_count,
  292. status: status,
  293. submitted_at: submitted_at,
  294. flowers_count: flowers_count,
  295. quality_score: quality_score,
  296. keywords: keywords(5),
  297. reading_time: reading_time_estimate
  298. }
  299. end
  300. end

app/services/daily_flower_stats_service.rb

0.0% lines covered

224 relevant lines. 0 lines covered and 224 lines missed.
    
  1. # 每日小红花统计服务
  2. # 自动统计前一天的小红花数据,生成排行榜,支持分享功能
  3. class DailyFlowerStatsService
  4. class << self
  5. # 生成指定日期的统计数据(默认为昨天)
  6. def generate_daily_stats(event, date = Date.yesterday, force: false)
  7. return { success: false, error: '活动不存在' } unless event
  8. return { success: false, error: '指定日期不是活动日' } unless event_reading_day?(event, date)
  9. # 检查是否已存在统计数据
  10. if DailyFlowerStat.exists_for_date?(event, date) && !force
  11. return { success: false, error: '该日期统计数据已存在' }
  12. end
  13. # 获取前一天的小红花数据
  14. flowers = get_flowers_for_date(event, date)
  15. return { success: false, error: '该日期无小红花数据' } if flowers.empty?
  16. # 生成排行榜
  17. leaderboard = generate_leaderboard(flowers)
  18. # 计算统计数据
  19. stats_data = calculate_statistics(flowers, event, date)
  20. # 创建或更新统计记录
  21. stat = DailyFlowerStat.find_or_initialize_by(reading_event: event, stats_date: date)
  22. stat.update!(
  23. leaderboard_data: {
  24. rankings: leaderboard,
  25. generated_at: Time.current,
  26. date: date,
  27. flower_count: flowers.count
  28. },
  29. total_flowers_given: stats_data[:total_flowers_given],
  30. total_participants: stats_data[:total_participants],
  31. total_givers: stats_data[:total_givers],
  32. generated_at: Time.current,
  33. generated_by: 'system_auto',
  34. share_text: generate_share_text(event, date, leaderboard),
  35. share_image_url: generate_share_image_url(event, date)
  36. )
  37. {
  38. success: true,
  39. message: '每日统计生成成功',
  40. stat: stat.as_json_for_api,
  41. summary: {
  42. date: date,
  43. event: event.title,
  44. total_flowers: stats_data[:total_flowers_given],
  45. total_participants: stats_data[:total_participants],
  46. top_three: leaderboard.first(3).map do |entry|
  47. user = User.find_by(id: entry[:user_id])
  48. {
  49. rank: entry[:rank],
  50. user: user&.as_json_for_api,
  51. total_flowers: entry[:total_flowers]
  52. }
  53. end
  54. }
  55. }
  56. rescue => e
  57. Rails.logger.error "每日统计生成失败: #{e.message}"
  58. {
  59. success: false,
  60. error: '统计生成失败',
  61. details: e.message
  62. }
  63. end
  64. # 批量生成多日统计(用于历史数据补全)
  65. def generate_batch_stats(event, start_date, end_date = nil)
  66. return { success: false, error: '活动不存在' } unless event
  67. end_date ||= event.end_date
  68. start_date = [start_date, event.start_date].max
  69. results = []
  70. failed_dates = []
  71. (start_date..end_date).each do |date|
  72. next unless event_reading_day?(event, date)
  73. next if date >= Date.current # 不处理今天和未来的日期
  74. result = generate_daily_stats(event, date, force: false)
  75. if result[:success]
  76. results << { date: date, success: true }
  77. else
  78. failed_dates << { date: date, error: result[:error] }
  79. end
  80. end
  81. {
  82. success: failed_dates.empty?,
  83. message: "批量统计完成",
  84. results: {
  85. processed: results.count,
  86. successful: results.count,
  87. failed: failed_dates.count,
  88. successful_dates: results,
  89. failed_dates: failed_dates
  90. }
  91. }
  92. end
  93. # 自动生成昨天的统计数据(定时任务调用)
  94. def auto_generate_yesterday_stats
  95. events = ReadingEvent.where(status: [:in_progress, :approved])
  96. results = []
  97. events.each do |event|
  98. next unless event_reading_day?(event, Date.yesterday)
  99. result = generate_daily_stats(event, Date.yesterday, force: false)
  100. results << {
  101. event_id: event.id,
  102. event_title: event.title,
  103. date: Date.yesterday,
  104. success: result[:success],
  105. error: result[:error]
  106. }
  107. end
  108. successful = results.select { |r| r[:success] }.count
  109. failed = results.count - successful
  110. Rails.logger.info "自动每日统计完成: 成功 #{successful} 个, 失败 #{failed} 个"
  111. {
  112. success: failed == 0,
  113. message: "自动统计完成",
  114. summary: {
  115. total_events: results.count,
  116. successful: successful,
  117. failed: failed,
  118. results: results
  119. }
  120. }
  121. end
  122. # 获取活动的每日统计历史
  123. def get_event_stats_history(event, days: 30)
  124. return { error: '活动不存在' } unless event
  125. stats = DailyFlowerStat.for_event(event)
  126. .where(stats_date: (Date.current - days.days)..Date.current)
  127. .order(stats_date: :desc)
  128. {
  129. event: event.as_json_for_api,
  130. period: "#{Date.current - days.days} 至 #{Date.current}",
  131. stats: stats.map(&:as_json_for_api)
  132. }
  133. end
  134. # 获取指定日期的排行榜数据
  135. def get_leaderboard_for_date(event, date = Date.yesterday)
  136. return { error: '活动不存在' } unless event
  137. stat = DailyFlowerStat.find_by(reading_event: event, stats_date: date)
  138. return { error: '该日期无统计数据' } unless stat
  139. {
  140. success: true,
  141. date: date,
  142. event: event.as_json_for_api,
  143. leaderboard: stat.leaderboard,
  144. top_three: stat.top_three,
  145. statistics: {
  146. total_flowers_given: stat.total_flowers_given,
  147. total_participants: stat.total_participants,
  148. total_givers: stat.total_givers,
  149. share_count: stat.share_count
  150. },
  151. share_info: {
  152. image_url: stat.share_image_url || stat.generate_share_image_url,
  153. text: stat.share_text_for_wechat,
  154. share_count: stat.share_count
  155. },
  156. generated_at: stat.generated_at
  157. }
  158. end
  159. # 增加分享次数并返回分享信息
  160. def increment_share_count(event, date = Date.yesterday)
  161. stat = DailyFlowerStat.find_by(reading_event: event, stats_date: date)
  162. return { error: '统计数据不存在' } unless stat
  163. stat.increment_share_count!
  164. {
  165. success: true,
  166. share_count: stat.share_count,
  167. share_info: {
  168. image_url: stat.share_image_url || stat.generate_share_image_url,
  169. text: stat.share_text_for_wechat
  170. }
  171. }
  172. end
  173. # 生成分享图片URL(占位符)
  174. def generate_share_image_url(event, date)
  175. # 这里可以集成第三方图片生成服务
  176. timestamp = Time.current.to_i
  177. base_url = Rails.application.config.base_url || 'http://localhost:3000'
  178. "#{base_url}/share-images/daily-flower-stats/#{event.id}/#{date}?t=#{timestamp}"
  179. end
  180. private
  181. # 获取指定日期的小红花数据
  182. def get_flowers_for_date(event, date)
  183. # 获取指定日期范围内的小红花
  184. start_time = date.beginning_of_day
  185. end_time = date.end_of_day
  186. Flower.joins(:recipient)
  187. .joins(check_in: :event_enrollment)
  188. .where(event_enrollments: { reading_event_id: event.id })
  189. .where('flowers.created_at >= ? AND flowers.created_at <= ?', start_time, end_time)
  190. .includes(:giver, :recipient, :check_in)
  191. end
  192. # 生成排行榜
  193. def generate_leaderboard(flowers)
  194. # 按接收者分组统计小红花数量
  195. flower_stats = flowers.group_by(&:recipient_id)
  196. .map do |recipient_id, user_flowers|
  197. recipient = User.find_by(id: recipient_id)
  198. next unless recipient
  199. {
  200. user_id: recipient_id,
  201. nickname: recipient.nickname,
  202. avatar_url: recipient.avatar_url,
  203. total_flowers: user_flowers.sum(&:amount),
  204. flowers_received: user_flowers.count,
  205. flowers_given: flowers.where(giver_id: recipient_id).count,
  206. check_ins: user_flowers.map(&:check_in).uniq.count,
  207. last_flower_at: user_flowers.maximum(:created_at)
  208. }
  209. end
  210. .compact
  211. .sort_by { |entry| -entry[:total_flowers] }
  212. .each_with_index.map { |entry, index| entry.merge(rank: index + 1) }
  213. end
  214. # 计算统计数据
  215. def calculate_statistics(flowers, event, date)
  216. {
  217. total_flowers_given: flowers.sum(&:amount),
  218. total_participants: flowers.map(&:recipient_id).uniq.count,
  219. total_givers: flowers.map(&:giver_id).uniq.count,
  220. average_flowers_per_user: flowers.count > 0 ? (flowers.sum(&:amount).to_f / flowers.map(&:recipient_id).uniq.count).round(2) : 0
  221. }
  222. end
  223. # 生成分享文案
  224. def generate_share_text(event, date, leaderboard)
  225. return '' if leaderboard.empty?
  226. text = "🌸 #{event.title} #{date.strftime('%m月%d日')}小红花排行榜\n\n"
  227. text += "🏆 今日小红花TOP3:\n"
  228. leaderboard.first(3).each_with_index do |entry, index|
  229. emoji = ['🥇', '🥈', '🥉'][index]
  230. text += "#{emoji} #{entry[:nickname]} - #{entry[:total_flowers]}朵\n"
  231. end
  232. text += "\n💝 #{leaderboard.first[:total_flowers]}朵小红花来自#{leaderboard.count}位小伙伴的鼓励!"
  233. text += "\n#读书打卡 #小红花 #共读成长"
  234. text
  235. end
  236. # 检查指定日期是否是活动阅读日
  237. def event_reading_day?(event, date)
  238. return false unless event.start_date && event.end_date
  239. return false if date < event.start_date || date > event.end_date
  240. # 如果设置周末休息,跳过周末
  241. if event.weekend_rest && (date.saturday? || date.sunday?)
  242. return false
  243. end
  244. true
  245. end
  246. end
  247. end

app/services/domain_events_service.rb

53.19% lines covered

47 relevant lines. 25 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. # DomainEventsService - 领域事件服务
  3. # 负责管理领域事件的发布和订阅,解耦服务间的依赖关系
  4. 1 class DomainEventsService
  5. 1 class << self
  6. # 发布事件
  7. 1 def publish(event_name, payload = {})
  8. event = DomainEvent.new(event_name, payload)
  9. Rails.logger.info "发布领域事件: #{event_name} - #{payload.inspect}"
  10. # 同步执行订阅者
  11. ActiveSupport::Notifications.instrument("domain_event.#{event_name}", payload) do
  12. subscribers = find_subscribers(event_name)
  13. subscribers.each { |subscriber| subscriber.call(event) }
  14. end
  15. event
  16. end
  17. # 订阅事件
  18. 1 def subscribe(event_name, subscriber_class = nil, &block)
  19. 9 subscriber = if block_given?
  20. block
  21. 9 elsif subscriber_class
  22. 9 if subscriber_class.respond_to?(:handle)
  23. 9 subscriber_class.method(:handle)
  24. else
  25. raise ArgumentError, "订阅者类必须实现handle方法"
  26. end
  27. else
  28. raise ArgumentError, "必须提供订阅者类或代码块"
  29. end
  30. 9 subscribers[event_name] ||= []
  31. 9 subscribers[event_name] << subscriber
  32. 9 Rails.logger.info "注册事件订阅: #{event_name} -> #{subscriber_class || '匿名订阅者'}"
  33. end
  34. # 取消订阅
  35. 1 def unsubscribe(event_name, subscriber)
  36. subscribers[event_name]&.delete(subscriber)
  37. end
  38. # 获取事件订阅者
  39. 1 def subscribers_for(event_name)
  40. subscribers[event_name] || []
  41. end
  42. # 清除所有订阅者(主要用于测试)
  43. 1 def clear_subscribers!
  44. @subscribers = {}
  45. end
  46. # 获取所有事件类型
  47. 1 def event_types
  48. subscribers.keys
  49. end
  50. 1 private
  51. 1 def subscribers
  52. 18 @subscribers ||= {}
  53. end
  54. 1 def find_subscribers(event_name)
  55. subscribers[event_name] || []
  56. end
  57. end
  58. # 领域事件类
  59. 1 class DomainEvent
  60. 1 attr_reader :name, :payload, :timestamp
  61. 1 def initialize(name, payload = {})
  62. @name = name
  63. @payload = payload.with_indifferent_access
  64. @timestamp = Time.current
  65. end
  66. 1 def data(key = nil)
  67. if key
  68. @payload[key]
  69. else
  70. @payload
  71. end
  72. end
  73. 1 def occurred_at
  74. @timestamp
  75. end
  76. 1 def to_s
  77. "DomainEvent(#{name}, #{payload}, #{@timestamp})"
  78. end
  79. end
  80. end

app/services/error_handling_service.rb

0.0% lines covered

333 relevant lines. 0 lines covered and 333 lines missed.
    
  1. # frozen_string_literal: true
  2. # 错误处理服务
  3. # 提供统一的错误处理、日志记录和用户友好的错误消息
  4. class ErrorHandlingService
  5. class << self
  6. # 处理API错误并返回标准化响应
  7. # @param error [Exception] 错误对象
  8. # @param context [Hash] 错误上下文信息
  9. # @return [Hash] 标准化的错误响应
  10. def handle_api_error(error, context = {})
  11. error_info = classify_error(error)
  12. # 记录错误日志
  13. log_error(error, context, error_info)
  14. # 清除相关的缓存
  15. clear_related_cache(context) if error_info[:clear_cache]
  16. # 发送错误通知(如果是严重错误)
  17. notify_error(error, context, error_info) if error_info[:notify]
  18. # 返回用户友好的错误响应
  19. format_error_response(error_info, error, context)
  20. end
  21. # 验证错误处理
  22. # @param errors [ActiveModel::Errors] 验证错误对象
  23. # @param context [Hash] 上下文信息
  24. # @return [Hash] 标准化的验证错误响应
  25. def handle_validation_errors(errors, context = {})
  26. error_details = errors.details.transform_values do |details|
  27. details.map { |detail| detail[:error].to_s.humanize }
  28. end
  29. response = {
  30. success: false,
  31. error_type: 'validation_error',
  32. message: '请求参数验证失败',
  33. errors: error_details,
  34. timestamp: Time.current.iso8601
  35. }
  36. # 添加请求ID
  37. if RequestStore.store[:request_id]
  38. response[:request_id] = RequestStore.store[:request_id]
  39. end
  40. # 记录验证错误日志
  41. Rails.logger.warn "验证错误: #{context[:action]} - #{error_details}"
  42. response
  43. end
  44. # 资源未找到错误处理
  45. # @param resource_type [String] 资源类型
  46. # @param resource_id [String, Integer] 资源ID
  47. # @param context [Hash] 上下文信息
  48. # @return [Hash] 标准化的未找到响应
  49. def handle_not_found_error(resource_type, resource_id = nil, context = {})
  50. message = if resource_id
  51. "#{resource_type.humanize} (ID: #{resource_id}) 不存在"
  52. else
  53. "#{resource_type.humanize} 不存在"
  54. end
  55. response = {
  56. success: false,
  57. error_type: 'not_found',
  58. message: message,
  59. resource_type: resource_type,
  60. resource_id: resource_id,
  61. timestamp: Time.current.iso8601
  62. }
  63. # 添加请求ID
  64. if RequestStore.store[:request_id]
  65. response[:request_id] = RequestStore.store[:request_id]
  66. end
  67. # 记录404日志
  68. Rails.logger.info "404错误: #{context[:action]} - #{message}"
  69. response
  70. end
  71. # 权限错误处理
  72. # @param action [String] 请求的操作
  73. # @param resource [String] 资源信息
  74. # @param context [Hash] 上下文信息
  75. # @return [Hash] 标准化的权限错误响应
  76. def handle_authorization_error(action, resource = nil, context = {})
  77. message = if resource
  78. "您没有权限执行此操作: #{action} #{resource}"
  79. else
  80. "您没有权限执行此操作: #{action}"
  81. end
  82. response = {
  83. success: false,
  84. error_type: 'authorization_error',
  85. message: message,
  86. required_permission: action,
  87. timestamp: Time.current.iso8601
  88. }
  89. # 添加用户信息
  90. if context[:user]
  91. response[:user_info] = {
  92. id: context[:user].id,
  93. role: context[:user].role_as_string
  94. }
  95. end
  96. # 添加请求ID
  97. if RequestStore.store[:request_id]
  98. response[:request_id] = RequestStore.store[:request_id]
  99. end
  100. # 记录权限错误日志
  101. Rails.logger.warn "权限错误: #{context[:user]&.id} - #{message}"
  102. response
  103. end
  104. # 业务逻辑错误处理
  105. # @param message [String] 错误消息
  106. # @param error_code [String] 错误代码
  107. # @param context [Hash] 上下文信息
  108. # @return [Hash] 标准化的业务错误响应
  109. def handle_business_error(message, error_code = nil, context = {})
  110. response = {
  111. success: false,
  112. error_type: 'business_error',
  113. message: message,
  114. error_code: error_code,
  115. timestamp: Time.current.iso8601
  116. }
  117. # 添加请求ID
  118. if RequestStore.store[:request_id]
  119. response[:request_id] = RequestStore.store[:request_id]
  120. end
  121. # 记录业务错误日志
  122. Rails.logger.info "业务错误: #{context[:action]} - #{message}"
  123. response
  124. end
  125. # 服务不可用错误处理
  126. # @param service_name [String] 服务名称
  127. # @param context [Hash] 上下文信息
  128. # @return [Hash] 标准化的服务不可用响应
  129. def handle_service_unavailable_error(service_name, context = {})
  130. message = "#{service_name} 服务暂时不可用,请稍后再试"
  131. response = {
  132. success: false,
  133. error_type: 'service_unavailable',
  134. message: message,
  135. service_name: service_name,
  136. retry_after: 30, # 建议重试时间(秒)
  137. timestamp: Time.current.iso8601
  138. }
  139. # 添加请求ID
  140. if RequestStore.store[:request_id]
  141. response[:request_id] = RequestStore.store[:request_id]
  142. end
  143. # 记录服务不可用日志
  144. Rails.logger.error "服务不可用: #{service_name} - #{context[:action]}"
  145. response
  146. end
  147. # 限流错误处理
  148. # @param limit_info [Hash] 限流信息
  149. # @param context [Hash] 上下文信息
  150. # @return [Hash] 标准化的限流错误响应
  151. def handle_rate_limit_error(limit_info, context = {})
  152. response = {
  153. success: false,
  154. error_type: 'rate_limit_exceeded',
  155. message: '请求过于频繁,请稍后再试',
  156. limit_info: limit_info,
  157. timestamp: Time.current.iso8601
  158. }
  159. # 添加请求ID
  160. if RequestStore.store[:request_id]
  161. response[:request_id] = RequestStore.store[:request_id]
  162. end
  163. # 记录限流日志
  164. Rails.logger.warn "限流错误: #{context[:action]} - #{limit_info}"
  165. response
  166. end
  167. # 创建用户友好的错误消息
  168. # @param error_class [Class] 错误类
  169. # @param error_message [String] 原始错误消息
  170. # @param context [Hash] 上下文信息
  171. # @return [String] 用户友好的错误消息
  172. def create_user_friendly_message(error_class, error_message, context = {})
  173. case error_class.name
  174. when 'ActiveRecord::RecordNotFound'
  175. case context[:resource_type]
  176. when 'User'
  177. '用户不存在'
  178. when 'ReadingEvent'
  179. '活动不存在'
  180. when 'CheckIn'
  181. '打卡记录不存在'
  182. when 'Flower'
  183. '小红花不存在'
  184. else
  185. '记录不存在'
  186. end
  187. when 'ActiveRecord::RecordInvalid'
  188. '数据验证失败,请检查输入信息'
  189. when 'ActiveRecord::RecordNotSaved'
  190. '保存失败,请检查网络连接后重试'
  191. when 'ArgumentError'
  192. '请求参数不正确'
  193. when 'JWT::DecodeError'
  194. '登录信息无效,请重新登录'
  195. when 'NoMethodError'
  196. '功能暂时不可用'
  197. when 'StandardError'
  198. if error_message.include?('数据库') || error_message.include?('database')
  199. '数据服务暂时不可用,请稍后再试'
  200. elsif error_message.include?('网络') || error_message.include?('network')
  201. '网络连接异常,请检查网络后重试'
  202. elsif error_message.include?('超时') || error_message.include?('timeout')
  203. '请求超时,请稍后再试'
  204. else
  205. '系统暂时异常,请稍后再试'
  206. end
  207. else
  208. '系统暂时异常,请稍后再试'
  209. end
  210. end
  211. # 错误分类
  212. # @param error [Exception] 错误对象
  213. # @return [Hash] 错误分类信息
  214. def classify_error(error)
  215. base_info = {
  216. class_name: error.class.name,
  217. message: error.message,
  218. backtrace: error.backtrace&.first(5)
  219. }
  220. case error
  221. when ActiveRecord::RecordNotFound
  222. base_info.merge(
  223. severity: :low,
  224. user_friendly: true,
  225. http_status: 404,
  226. clear_cache: false,
  227. notify: false
  228. )
  229. when ActiveRecord::RecordInvalid, ArgumentError
  230. base_info.merge(
  231. severity: :low,
  232. user_friendly: true,
  233. http_status: 400,
  234. clear_cache: false,
  235. notify: false
  236. )
  237. when JWT::DecodeError, ActionController::InvalidAuthenticityToken
  238. base_info.merge(
  239. severity: :medium,
  240. user_friendly: true,
  241. http_status: 401,
  242. clear_cache: false,
  243. notify: false
  244. )
  245. when ActiveRecord::RecordNotSaved, ActiveRecord::StatementInvalid
  246. base_info.merge(
  247. severity: :medium,
  248. user_friendly: true,
  249. http_status: 422,
  250. clear_cache: false,
  251. notify: true
  252. )
  253. when StandardError
  254. if error.message.include?('超时') || error.message.include?('timeout')
  255. base_info.merge(
  256. severity: :medium,
  257. user_friendly: true,
  258. http_status: 408,
  259. clear_cache: false,
  260. notify: false
  261. )
  262. elsif error.message.include?('权限') || error.message.include?('permission')
  263. base_info.merge(
  264. severity: :medium,
  265. user_friendly: true,
  266. http_status: 403,
  267. clear_cache: false,
  268. notify: false
  269. )
  270. else
  271. base_info.merge(
  272. severity: :high,
  273. user_friendly: false,
  274. http_status: 500,
  275. clear_cache: true,
  276. notify: true
  277. )
  278. end
  279. else
  280. base_info.merge(
  281. severity: :high,
  282. user_friendly: false,
  283. http_status: 500,
  284. clear_cache: true,
  285. notify: true
  286. )
  287. end
  288. end
  289. # 记录错误日志
  290. # @param error [Exception] 错误对象
  291. # @param context [Hash] 上下文信息
  292. # @param error_info [Hash] 错误分类信息
  293. def log_error(error, context, error_info)
  294. log_data = {
  295. error_class: error_info[:class_name],
  296. error_message: error_info[:message],
  297. severity: error_info[:severity],
  298. context: context,
  299. timestamp: Time.current,
  300. backtrace: error_info[:backtrace]
  301. }
  302. # 添加用户信息
  303. if context[:user]
  304. log_data[:user_id] = context[:user].id
  305. log_data[:user_role] = context[:user].role_as_string
  306. end
  307. # 根据严重程度选择日志级别
  308. case error_info[:severity]
  309. when :low
  310. Rails.logger.info "错误日志: #{log_data}"
  311. when :medium
  312. Rails.logger.warn "警告日志: #{log_data}"
  313. when :high
  314. Rails.logger.error "错误日志: #{log_data}"
  315. end
  316. # 发送到外部错误监控服务
  317. send_to_error_monitoring(log_data) if error_info[:notify]
  318. end
  319. # 清除相关缓存
  320. # @param context [Hash] 上下文信息
  321. def clear_related_cache(context)
  322. return unless context[:user]
  323. case context[:action]
  324. when 'create', 'update', 'destroy'
  325. CacheService.clear_user_cache(context[:user])
  326. end
  327. end
  328. # 发送错误通知
  329. # @param error [Exception] 错误对象
  330. # @param context [Hash] 上下文信息
  331. # @param error_info [Hash] 错误分类信息
  332. def notify_error(error, context, error_info)
  333. return unless Rails.env.production? # 只在生产环境发送通知
  334. # 这里可以集成邮件、Slack、钉钉等通知服务
  335. error_data = {
  336. error_class: error_info[:class_name],
  337. error_message: error_info[:message],
  338. context: context,
  339. timestamp: Time.current,
  340. environment: Rails.env
  341. }
  342. # 示例:发送到Slack(需要配置webhook)
  343. # SlackNotifier.notify_error(error_data) if defined?(SlackNotifier)
  344. end
  345. # 格式化错误响应
  346. # @param error_info [Hash] 错误分类信息
  347. # @param error [Exception] 错误对象
  348. # @param context [Hash] 上下文信息
  349. # @return [Hash] 标准化的错误响应
  350. def format_error_response(error_info, error, context)
  351. user_friendly_message = create_user_friendly_message(
  352. error.class,
  353. error_info[:message],
  354. context
  355. )
  356. response = {
  357. success: false,
  358. error_type: error_info[:class_name].underscore,
  359. message: user_friendly_message,
  360. timestamp: Time.current.iso8601
  361. }
  362. # 开发环境显示详细信息
  363. if Rails.env.development?
  364. response[:debug] = {
  365. original_error: error_info[:message],
  366. backtrace: error_info[:backtrace],
  367. context: context
  368. }
  369. end
  370. # 添加请求ID
  371. if RequestStore.store[:request_id]
  372. response[:request_id] = RequestStore.store[:request_id]
  373. end
  374. response
  375. end
  376. # 发送错误到外部监控服务
  377. # @param error_data [Hash] 错误数据
  378. def send_to_error_monitoring(error_data)
  379. # 这里可以集成Sentry、Bugsnag、Rollbar等错误监控服务
  380. # 示例:
  381. # Sentry.capture_exception(error_data[:error], extra: error_data)
  382. end
  383. # 异常处理装饰器
  384. # @param operation [Symbol] 操作类型
  385. # @param context [Hash] 上下文信息
  386. # @param options [Hash] 选项
  387. # @yield 要执行的操作
  388. # @return [Object] 操作结果或错误响应
  389. def with_error_handling(operation, context = {}, options = {})
  390. begin
  391. yield
  392. rescue => e
  393. if options[:return_response]
  394. handle_api_error(e, context.merge(operation: operation))
  395. else
  396. raise e
  397. end
  398. end
  399. end
  400. # 批量错误处理
  401. # @param operations [Array] 操作数组
  402. # @param context [Hash] 上下文信息
  403. # @return [Hash] 批量处理结果
  404. def handle_batch_errors(operations, context = {})
  405. results = {
  406. successful: [],
  407. failed: [],
  408. total: operations.length
  409. }
  410. operations.each_with_index do |operation, index|
  411. begin
  412. result = yield(operation) if block_given?
  413. results[:successful] << {
  414. index: index,
  415. operation: operation,
  416. result: result
  417. }
  418. rescue => e
  419. error_response = handle_api_error(e, context.merge(operation: operation))
  420. results[:failed] << {
  421. index: index,
  422. operation: operation,
  423. error: error_response
  424. }
  425. end
  426. end
  427. results
  428. end
  429. end
  430. end

app/services/event_enrollment_service.rb

0.0% lines covered

51 relevant lines. 0 lines covered and 51 lines missed.
    
  1. # frozen_string_literal: true
  2. # EventEnrollmentService - 活动报名管理服务
  3. # 负责活动报名、验证、人数限制等业务逻辑
  4. class EventEnrollmentService < ApplicationService
  5. attr_reader :event, :user, :enrollment
  6. def initialize(event:, user:)
  7. super()
  8. @event = event
  9. @user = user
  10. @enrollment = nil
  11. end
  12. # 主要调用方法
  13. def call
  14. handle_errors do
  15. # 检查活动是否已审批
  16. unless event.approved?
  17. return failure!("活动尚未审批通过,无法报名")
  18. end
  19. # 检查是否已报名
  20. if user.enrollments.exists?(reading_event: event)
  21. return failure!("您已经报名该活动")
  22. end
  23. # 检查人数限制
  24. if event.enrollments.count >= event.max_participants
  25. return failure!("活动已满员")
  26. end
  27. # 检查活动状态
  28. unless event.enrolling?
  29. return failure!("当前活动不在报名期间")
  30. end
  31. # 创建报名记录
  32. create_enrollment
  33. end
  34. end
  35. # 类方法:快速报名
  36. def self.enroll_user!(event, user)
  37. new(event: event, user: user).call
  38. end
  39. private
  40. # 创建报名记录
  41. def create_enrollment
  42. @enrollment = user.enrollments.create!(
  43. reading_event: event,
  44. paid_amount: event.enrollment_fee
  45. )
  46. # 如果是随机分配模式且有足够参与者,自动分配领读人
  47. if event.leader_assignment_type == 'random' && event.enrollments.count >= 3
  48. event.assign_daily_leaders!
  49. end
  50. success!({
  51. message: "报名成功",
  52. enrollment_data: {
  53. id: @enrollment.id,
  54. user_id: @enrollment.user_id,
  55. reading_event_id: @enrollment.reading_event_id,
  56. payment_status: @enrollment.payment_status,
  57. role: @enrollment.role,
  58. paid_amount: @enrollment.paid_amount,
  59. created_at: @enrollment.created_at
  60. }
  61. })
  62. end
  63. end

app/services/event_management_service.rb

0.0% lines covered

99 relevant lines. 0 lines covered and 99 lines missed.
    
  1. # frozen_string_literal: true
  2. # EventManagementService - 活动生命周期管理服务
  3. # 负责活动的创建、审批、拒绝、完成等核心业务逻辑
  4. class EventManagementService < ApplicationService
  5. attr_reader :event, :admin_user, :action
  6. def initialize(event:, admin_user: nil, action: nil)
  7. super()
  8. @event = event
  9. @admin_user = admin_user
  10. @action = action
  11. end
  12. # 主要调用方法
  13. def call
  14. handle_errors do
  15. case action
  16. when :approve
  17. approve_event
  18. when :reject
  19. reject_event
  20. when :complete
  21. complete_event
  22. else
  23. failure!("不支持的操作: #{action}")
  24. end
  25. end
  26. self # 返回service实例
  27. end
  28. # 类方法:审批活动
  29. def self.approve_event!(event, admin_user)
  30. new(event: event, admin_user: admin_user, action: :approve).call
  31. end
  32. # 类方法:拒绝活动
  33. def self.reject_event!(event, admin_user)
  34. new(event: event, admin_user: admin_user, action: :reject).call
  35. end
  36. # 类方法:完成活动
  37. def self.complete_event!(event, current_user)
  38. new(event: event, admin_user: current_user, action: :complete).call
  39. end
  40. private
  41. # 审批活动
  42. def approve_event
  43. return failure!("管理员用户不能为空") unless admin_user
  44. # 检查管理员权限
  45. unless admin_user.can_approve_events?
  46. return failure!("用户 #{admin_user.nickname} 没有审批权限")
  47. end
  48. # 检查活动状态
  49. unless event.pending_approval?
  50. return failure!("只能审批待审批的活动")
  51. end
  52. # 执行审批
  53. event.transaction do
  54. event.approve!(admin_user)
  55. # 如果是随机分配模式且有足够参与者,自动分配领读人
  56. if event.leader_assignment_type == 'random' && event.enrollments.count >= 3
  57. event.assign_daily_leaders!
  58. end
  59. end
  60. success!({
  61. message: "活动审批通过",
  62. event_data: {
  63. 'id' => event.id,
  64. 'title' => event.title,
  65. 'status' => event.status_symbol,
  66. 'approval_status' => event.approval_status_symbol,
  67. 'approved_by' => admin_user.nickname,
  68. 'approved_at' => event.approved_at
  69. }
  70. })
  71. end
  72. # 拒绝活动
  73. def reject_event
  74. return failure!("管理员用户不能为空") unless admin_user
  75. # 检查管理员权限
  76. unless admin_user.can_approve_events?
  77. return failure!("用户 #{admin_user.nickname} 没有审批权限")
  78. end
  79. # 检查活动状态
  80. unless event.pending_approval?
  81. return failure!("只能拒绝待审批的活动")
  82. end
  83. # 执行拒绝
  84. event.reject!(admin_user)
  85. success!({
  86. message: "活动已被拒绝",
  87. event_data: {
  88. 'id' => event.id,
  89. 'title' => event.title,
  90. 'status' => event.status_symbol,
  91. 'approval_status' => event.approval_status_symbol,
  92. 'rejected_by' => admin_user.nickname,
  93. 'rejected_at' => event.approved_at
  94. }
  95. })
  96. end
  97. # 完成活动
  98. def complete_event
  99. # 检查活动状态(先检查状态,再检查权限)
  100. if event.completed?
  101. return failure!("活动已经结束")
  102. end
  103. # 检查用户权限 - 只有活动小组长可以结束活动
  104. unless event.current_leader?(admin_user)
  105. return failure!("只有活动小组长可以结束活动")
  106. end
  107. # 执行活动完成
  108. event.complete_event!
  109. success!({
  110. message: "活动已成功结束",
  111. event_data: {
  112. 'id' => event.id,
  113. 'title' => event.title,
  114. 'status' => event.status_symbol,
  115. 'completed_at' => Time.current
  116. }
  117. })
  118. end
  119. end

app/services/event_subscribers/notification_event_subscriber.rb

18.06% lines covered

72 relevant lines. 13 lines covered and 59 lines missed.
    
  1. # frozen_string_literal: true
  2. # NotificationEventSubscriber - 通知事件订阅者
  3. # 监听各种领域事件并发送相应的通知
  4. 1 class NotificationEventSubscriber
  5. 1 class << self
  6. # 处理领域事件
  7. 1 def handle(event)
  8. case event.name
  9. when 'flower.given'
  10. handle_flower_given(event)
  11. when 'flower.comment_created'
  12. handle_flower_comment(event)
  13. when 'post.created'
  14. handle_post_created(event)
  15. when 'post.updated'
  16. handle_post_updated(event)
  17. when 'post.moderated'
  18. handle_post_moderated(event)
  19. when 'report.created'
  20. handle_report_created(event)
  21. when 'report.processed'
  22. handle_report_processed(event)
  23. when 'event.enrollment.created'
  24. handle_event_enrollment_created(event)
  25. when 'event.approval.required'
  26. handle_event_approval_required(event)
  27. else
  28. Rails.logger.warn "未知事件类型: #{event.name}"
  29. end
  30. rescue => e
  31. Rails.logger.error "处理事件失败 #{event.name}: #{e.message}"
  32. Rails.logger.error e.backtrace.join("\n")
  33. end
  34. 1 private
  35. # 处理小红花赠送事件
  36. 1 def handle_flower_given(event)
  37. giver = event.data(:giver)
  38. recipient = event.data(:recipient)
  39. flower = event.data(:flower)
  40. return unless giver && recipient && flower
  41. NotificationService.send_flower_notification(recipient, giver, flower)
  42. end
  43. # 处理小红花评论事件
  44. 1 def handle_flower_comment(event)
  45. flower = event.data(:flower)
  46. commenter = event.data(:commenter)
  47. comment = event.data(:comment)
  48. return unless flower && commenter && comment
  49. NotificationService.send_comment_notification(flower.recipient, commenter, comment)
  50. end
  51. # 处理帖子创建事件
  52. 1 def handle_post_created(event)
  53. post = event.data(:post)
  54. user = event.data(:user)
  55. return unless post && user
  56. # 可以在这里发送帖子创建通知给关注者等
  57. # NotificationService.post_created_notification(post, user)
  58. Rails.logger.info "帖子创建事件: #{post.title} by #{user.nickname}"
  59. end
  60. # 处理帖子更新事件
  61. 1 def handle_post_updated(event)
  62. post = event.data(:post)
  63. user = event.data(:user)
  64. return unless post && user
  65. # NotificationService.post_updated_notification(post, user)
  66. Rails.logger.info "帖子更新事件: #{post.title} by #{user.nickname}"
  67. end
  68. # 处理帖子审核事件
  69. 1 def handle_post_moderated(event)
  70. post = event.data(:post)
  71. moderator = event.data(:moderator)
  72. action = event.data(:action)
  73. reason = event.data(:reason)
  74. return unless post && moderator
  75. case action
  76. when 'pin'
  77. # NotificationService.post_pinned_notification(post, moderator)
  78. Rails.logger.info "帖子置顶事件: #{post.title} by #{moderator.nickname}"
  79. when 'hide'
  80. # NotificationService.post_hidden_notification(post, moderator, reason)
  81. Rails.logger.info "帖子隐藏事件: #{post.title} by #{moderator.nickname}, 原因: #{reason}"
  82. when 'delete'
  83. # NotificationService.post_deleted_notification(post, moderator, reason)
  84. Rails.logger.info "帖子删除事件: #{post.title} by #{moderator.nickname}, 原因: #{reason}"
  85. end
  86. end
  87. # 处理举报创建事件
  88. 1 def handle_report_created(event)
  89. report = event.data(:report)
  90. reporter = event.data(:reporter)
  91. return unless report && reporter
  92. # 发送通知给管理员
  93. NotificationService.send_bulk_notifications(
  94. User.where(role: %w[admin moderator]),
  95. reporter,
  96. report,
  97. 'report_created',
  98. '新的举报',
  99. "用户 #{reporter.nickname} 提交了新的举报,请及时处理。"
  100. )
  101. end
  102. # 处理举报处理事件
  103. 1 def handle_report_processed(event)
  104. report = event.data(:report)
  105. processor = event.data(:processor)
  106. action = event.data(:action)
  107. return unless report && processor
  108. # 通知举报者处理结果
  109. if report.user
  110. NotificationService.send_system_notification(
  111. report.user,
  112. '举报处理结果',
  113. "您提交的举报已被处理,处理结果:#{action}",
  114. actor: processor,
  115. notifiable: report
  116. )
  117. end
  118. end
  119. # 处理活动报名事件
  120. 1 def handle_event_enrollment_created(event)
  121. enrollment = event.data(:enrollment)
  122. user = event.data(:user)
  123. event = event.data(:event)
  124. return unless enrollment && user && event
  125. # 通知活动组织者
  126. NotificationService.send_activity_update_notification(
  127. event.user, # 活动创建者
  128. user,
  129. event,
  130. 'new_enrollment',
  131. "#{user.nickname} 报名了您的活动"
  132. )
  133. end
  134. # 处理活动审批需求事件
  135. 1 def handle_event_approval_required(event)
  136. event = event.data(:event)
  137. submitter = event.data(:submitter)
  138. return unless event && submitter
  139. # 通知所有管理员审批
  140. NotificationService.send_bulk_notifications(
  141. User.where(role: 'admin'),
  142. submitter,
  143. event,
  144. 'event_approval_required',
  145. '活动审批',
  146. "活动 #{event.title} 需要审批"
  147. )
  148. end
  149. end
  150. end

app/services/flower_certificate_service.rb

0.0% lines covered

178 relevant lines. 0 lines covered and 178 lines missed.
    
  1. # 小红花证书服务
  2. # 负责生成和管理小红花相关的证书
  3. class FlowerCertificateService
  4. class << self
  5. # 活动结束时生成小红花证书
  6. def finalize_event_flower_certificates(event)
  7. return { success: false, error: '活动未结束' } unless event.status == 'completed'
  8. return { success: false, error: '活动没有参与者' } if event.participants.empty?
  9. certificates = generate_top_three_certificates(event)
  10. {
  11. success: true,
  12. event: event.title,
  13. certificates: certificates.map do |cert|
  14. {
  15. rank: cert.rank_display,
  16. user: cert.user.as_json_for_api,
  17. total_flowers: cert.total_flowers,
  18. certificate_id: cert.certificate_id,
  19. honor_level: cert.honor_level,
  20. share_url: cert.share_url
  21. }
  22. end
  23. }
  24. end
  25. # 生成活动前三名证书
  26. def generate_top_three_certificates(event)
  27. return [] unless event.participants.any?
  28. # 计算每个参与者的小红花总数
  29. flower_stats = calculate_event_flower_statistics(event)
  30. # 排序并取前三名
  31. top_three = flower_stats.sort_by { |user_id, flowers| -flowers }
  32. .first(3)
  33. certificates = []
  34. top_three.each_with_index do |(user_id, flowers), index|
  35. user = User.find(user_id)
  36. rank = index + 1
  37. # 生成证书
  38. cert = FlowerCertificate.create!(
  39. user: user,
  40. reading_event: event,
  41. certificate_type: "flower_top#{rank}",
  42. rank: rank,
  43. total_flowers: flowers,
  44. certificate_number: generate_certificate_number(event, rank),
  45. honor_level: calculate_honor_level(flowers),
  46. issued_at: Time.current,
  47. expires_at: event.end_date + 1.year
  48. )
  49. certificates << cert
  50. # 记录到参与者的证书列表
  51. participation_cert = ParticipationCertificate.create!(
  52. user: user,
  53. reading_event: event,
  54. certificate_type: "flower_top#{rank}",
  55. certificate_number: cert.certificate_number,
  56. issued_at: cert.issued_at
  57. )
  58. # 发送通知
  59. send_certificate_notification(user, cert, event)
  60. end
  61. certificates
  62. end
  63. # 获取活动的前三名排行榜
  64. def get_event_top_three(event)
  65. return { error: '活动未结束' } unless event.status == 'completed'
  66. certificates = FlowerCertificate.for_event(event).ranked
  67. {
  68. event: event.title,
  69. total_participants: event.participants.count,
  70. top_three: certificates.map do |cert|
  71. {
  72. rank: cert.rank_display,
  73. user: cert.user.as_json_for_api,
  74. total_flowers: cert.total_flowers,
  75. honor_level: cert.honor_level,
  76. certificate_id: cert.certificate_id
  77. }
  78. end,
  79. generated_at: certificates.first&.created_at
  80. }
  81. end
  82. # 获取用户的所有小红花证书
  83. def get_user_certificates(user)
  84. certificates = FlowerCertificate.for_user_all(user)
  85. {
  86. user: user.as_json_for_api,
  87. total_certificates: certificates.count,
  88. certificates: certificates.map do |cert|
  89. {
  90. event: cert.reading_event.title,
  91. rank: cert.rank_display,
  92. total_flowers: cert.total_flowers,
  93. honor_level: cert.honor_level,
  94. certificate_id: cert.certificate_id,
  95. earned_at: cert.created_at,
  96. is_valid: cert.valid_certificate?,
  97. share_url: cert.share_url
  98. }
  99. end
  100. }
  101. end
  102. # 验证证书有效性
  103. def validate_certificate(certificate_id)
  104. cert = FlowerCertificate.find_by(certificate_id: certificate_id)
  105. return { valid: false, error: '证书不存在' } unless cert
  106. {
  107. valid: cert.valid_certificate?,
  108. certificate: cert,
  109. user: cert.user.as_json_for_api,
  110. event: cert.reading_event.as_json_for_api,
  111. expires_at: cert.expires_at,
  112. days_until_expiry: cert.days_until_expiry
  113. }
  114. end
  115. # 重新生成证书(用于修正错误)
  116. def regenerate_certificate(certificate_id, admin_user)
  117. cert = FlowerCertificate.find_by(certificate_id: certificate_id)
  118. return { success: false, error: '证书不存在' } unless cert
  119. # 记录重新生成日志
  120. Rails.logger.info "证书重新生成: #{certificate_id} by #{admin_user&.nickname}"
  121. # 生成新的证书编号
  122. new_certificate_number = generate_certificate_number(cert.reading_event, cert.rank)
  123. cert.update!(
  124. certificate_number: new_certificate_number,
  125. issued_at: Time.current,
  126. expires_at: cert.reading_event.end_date + 1.year,
  127. regenerated_at: Time.current,
  128. regenerated_by: admin_user&.id
  129. )
  130. {
  131. success: true,
  132. certificate: cert,
  133. message: '证书已重新生成'
  134. }
  135. end
  136. # 批量生成参与证书
  137. def batch_generate_participation_certificates(event, user_ids = nil)
  138. return { success: false, error: '活动未结束' } unless event.status == 'completed'
  139. target_users = user_ids ? User.where(id: user_ids) : event.participants
  140. certificates = []
  141. target_users.each do |user|
  142. enrollment = event.event_enrollments.find_by(user: user)
  143. next unless enrollment&.is_completed?
  144. # 生成完成证书
  145. cert = ParticipationCertificate.create!(
  146. user: user,
  147. reading_event: event,
  148. certificate_type: 'completion',
  149. certificate_number: generate_certificate_number(event, 'completion'),
  150. issued_at: Time.current,
  151. expires_at: event.end_date + 2.years
  152. )
  153. certificates << cert
  154. end
  155. {
  156. success: true,
  157. generated_count: certificates.count,
  158. certificates: certificates
  159. }
  160. end
  161. private
  162. # 计算活动中每个参与者的小红花统计
  163. def calculate_event_flower_statistics(event)
  164. flower_stats = {}
  165. event.check_ins.includes(:flowers, :user).each do |check_in|
  166. check_in.flowers.each do |flower|
  167. user_id = flower.recipient_id
  168. flower_stats[user_id] = (flower_stats[user_id] || 0) + flower.amount
  169. end
  170. end
  171. flower_stats
  172. end
  173. # 生成证书编号
  174. def generate_certificate_number(event, type_or_rank)
  175. prefix = event.id.to_s.rjust(4, '0')
  176. timestamp = Time.current.strftime('%Y%m%d')
  177. type_code = type_or_rank.is_a?(Integer) ? "TOP#{type_or_rank}" : type_or_rank.to_s.upcase.first(3)
  178. random_code = SecureRandom.hex(4).upcase
  179. "#{prefix}-#{timestamp}-#{type_code}-#{random_code}"
  180. end
  181. # 计算荣誉等级
  182. def calculate_honor_level(flowers)
  183. case flowers
  184. when 0..2
  185. 'bronze'
  186. when 3..5
  187. 'silver'
  188. when 6..10
  189. 'gold'
  190. else
  191. 'platinum'
  192. end
  193. end
  194. # 发送证书通知
  195. def send_certificate_notification(user, certificate, event)
  196. # 这里应该调用通知服务发送邮件或消息
  197. # NotificationService.send_certificate_notification(user, certificate, event)
  198. Rails.logger.info "证书通知已发送: 用户#{user.nickname}, 证书#{certificate.certificate_id}"
  199. end
  200. end
  201. end

app/services/flower_comment_service.rb

0.0% lines covered

209 relevant lines. 0 lines covered and 209 lines missed.
    
  1. # 小红花评论服务
  2. # 负责管理小红花的评论功能,包括创建、查询和权限管理
  3. class FlowerCommentService
  4. class << self
  5. # 为小红花添加评论
  6. def add_comment_to_flower(flower, user, content)
  7. return { success: false, error: '小红花不存在' } unless flower
  8. return { success: false, error: '用户不存在' } unless user
  9. return { success: false, error: '评论内容不能为空' } if content.blank?
  10. # 验证评论权限
  11. unless can_comment_on_flower?(flower, user)
  12. return { success: false, error: '您没有权限评论此小红花' }
  13. end
  14. # 内容验证
  15. unless valid_comment_content?(content)
  16. return { success: false, error: '评论内容长度应在2-1000字符之间' }
  17. end
  18. # 创建评论
  19. comment = flower.add_comment(user, content)
  20. # 发布小红花评论事件,解耦通知服务
  21. DomainEventsService.publish('flower.comment_created', {
  22. flower: flower,
  23. commenter: user,
  24. comment: comment
  25. })
  26. {
  27. success: true,
  28. comment: comment.as_json_for_api,
  29. message: '评论添加成功'
  30. }
  31. rescue => e
  32. Rails.logger.error "小红花评论添加失败: #{e.message}"
  33. {
  34. success: false,
  35. error: '评论添加失败,请重试',
  36. details: e.message
  37. }
  38. end
  39. # 获取小红花的评论列表
  40. def get_flower_comments(flower, page = 1, limit = 10, current_user: nil)
  41. return { success: false, error: '小红花不存在' } unless flower
  42. # 分页查询评论
  43. comments = flower.comments
  44. .includes(:user)
  45. .order(created_at: :desc)
  46. .offset((page - 1) * limit)
  47. .limit(limit)
  48. # 检查用户权限
  49. can_comment = current_user ? can_comment_on_flower?(flower, current_user) : false
  50. {
  51. success: true,
  52. flower: {
  53. id: flower.id,
  54. giver_display_name: flower.giver_display_name,
  55. recipient_display_name: flower.recipient_display_name,
  56. flower_type: flower.flower_type,
  57. created_at: flower.created_at
  58. },
  59. comments: comments.map do |comment|
  60. comment_data = comment.as_json_for_api
  61. comment_data[:can_edit] = current_user ? comment.can_edit?(current_user) : false
  62. comment_data
  63. end,
  64. pagination: {
  65. current_page: page,
  66. total_count: flower.comments_count,
  67. total_pages: (flower.comments_count.to_f / limit).ceil,
  68. has_next: (page * limit) < flower.comments_count,
  69. has_prev: page > 1
  70. },
  71. permissions: {
  72. can_comment: can_comment,
  73. total_comments: flower.comments_count
  74. }
  75. }
  76. end
  77. # 获取小红花的评论统计
  78. def get_flower_comment_stats(flower)
  79. return { success: false, error: '小红花不存在' } unless flower
  80. comments = flower.comments.includes(:user)
  81. # 计算统计数据
  82. stats = {
  83. total_count: comments.count,
  84. today_count: comments.where(created_at: Date.current.all_day).count,
  85. this_week_count: comments.where(created_at: Date.current.beginning_of_week..Date.current.end_of_week).count,
  86. unique_users: comments.distinct.count(:user_id),
  87. avg_comment_length: comments.average("LENGTH(content)")&.round(2) || 0
  88. }
  89. # 最活跃的评论者
  90. active_commenters = comments.joins(:user)
  91. .group('users.id', 'users.nickname')
  92. .order('COUNT(*) DESC')
  93. .limit(5)
  94. .count
  95. {
  96. success: true,
  97. flower_id: flower.id,
  98. stats: stats,
  99. active_commenters: active_commenters.map { |user_id, nickname, count|
  100. {
  101. user_id: user_id,
  102. nickname: nickname,
  103. comment_count: count
  104. }
  105. },
  106. latest_comment: comments.order(created_at: :desc).first&.as_json_for_api
  107. }
  108. end
  109. # 删除小红花评论
  110. def delete_flower_comment(flower, comment, current_user)
  111. return { success: false, error: '评论不存在' } unless comment
  112. return { success: false, error: '小红花不存在' } unless flower
  113. return { success: false, error: '用户不存在' } unless current_user
  114. # 检查删除权限
  115. unless can_delete_comment?(comment, current_user)
  116. return { success: false, error: '您没有权限删除此评论' }
  117. end
  118. # 删除评论
  119. comment.destroy
  120. {
  121. success: true,
  122. message: '评论已删除',
  123. remaining_comments: flower.comments_count
  124. }
  125. rescue => e
  126. Rails.logger.error "小红花评论删除失败: #{e.message}"
  127. {
  128. success: false,
  129. error: '评论删除失败,请重试',
  130. details: e.message
  131. }
  132. end
  133. # 批量删除小红花评论(管理员功能)
  134. def batch_delete_flower_comments(flower, comment_ids, admin_user)
  135. return { success: false, error: '需要管理员权限' } unless admin_user&.any_admin?
  136. return { success: false, error: '小红花不存在' } unless flower
  137. # 查找要删除的评论
  138. comments = flower.comments.where(id: comment_ids)
  139. deleted_count = 0
  140. failed_comments = []
  141. comments.each do |comment|
  142. if comment.destroy
  143. deleted_count += 1
  144. else
  145. failed_comments << comment.id
  146. end
  147. end
  148. {
  149. success: failed_comments.empty?,
  150. deleted_count: deleted_count,
  151. failed_count: failed_comments.length,
  152. failed_comment_ids: failed_comments,
  153. remaining_comments: flower.comments_count,
  154. message: failed_comments.empty? ? "所有评论已删除" : "部分评论删除失败"
  155. }
  156. rescue => e
  157. Rails.logger.error "批量删除小红花评论失败: #{e.message}"
  158. {
  159. success: false,
  160. error: '批量删除失败,请重试',
  161. details: e.message
  162. }
  163. end
  164. # 搜索小红花评论
  165. def search_flower_comments(flower, keyword, page = 1, limit = 10, current_user: nil)
  166. return { success: false, error: '小红花不存在' } unless flower
  167. return { success: false, error: '搜索关键词不能为空' } if keyword.blank?
  168. # 搜索评论
  169. comments = flower.comments
  170. .includes(:user)
  171. .where('content ILIKE ?', "%#{keyword}%")
  172. .order(created_at: :desc)
  173. .offset((page - 1) * limit)
  174. .limit(limit)
  175. {
  176. success: true,
  177. keyword: keyword,
  178. results: comments.map do |comment|
  179. comment_data = comment.as_json_for_api
  180. comment_data[:can_edit] = current_user ? comment.can_edit?(current_user) : false
  181. comment_data[:highlighted_content] = highlight_search_content(comment.content, keyword)
  182. comment_data
  183. end,
  184. pagination: {
  185. current_page: page,
  186. total_count: comments.count,
  187. total_pages: (comments.count.to_f / limit).ceil,
  188. has_next: (page * limit) < comments.count,
  189. has_prev: page > 1
  190. }
  191. }
  192. rescue => e
  193. Rails.logger.error "小红花评论搜索失败: #{e.message}"
  194. {
  195. success: false,
  196. error: '搜索失败,请重试',
  197. details: e.message
  198. }
  199. end
  200. private
  201. # 检查用户是否可以评论小红花
  202. def can_comment_on_flower?(flower, user)
  203. return false unless user && flower
  204. # 小红花接收者可以评论
  205. return true if flower.recipient_id == user.id
  206. # 小红花赠送者可以评论
  207. return true if flower.giver_id == user.id
  208. # 同一活动的参与者可以评论
  209. if flower.check_in && flower.check_in.reading_event
  210. event = flower.check_in.reading_event
  211. return true if event.participants.include?(user)
  212. end
  213. false
  214. end
  215. # 检查用户是否可以删除评论
  216. def can_delete_comment?(comment, user)
  217. return false unless user && comment
  218. # 评论作者可以删除自己的评论
  219. return true if comment.user_id == user.id
  220. # 管理员可以删除任何评论
  221. return true if user.any_admin?
  222. # 小红花接收者可以删除关于自己的小红花的评论
  223. if comment.commentable_type == 'Flower'
  224. flower = comment.commentable
  225. return true if flower.recipient_id == user.id
  226. end
  227. false
  228. end
  229. # 验证评论内容
  230. def valid_comment_content?(content)
  231. content_length = content.to_s.strip.length
  232. content_length >= 2 && content_length <= 1000
  233. end
  234. # 高亮搜索关键词
  235. def highlight_search_content(content, keyword)
  236. # 简单的关键词高亮实现
  237. content.gsub(/#{Regexp.escape(keyword)}/i, "**#{keyword}**")
  238. end
  239. # 发送小红花评论通知已移至事件订阅者中处理
  240. # 这样可以解耦FlowerCommentService和NotificationService的依赖关系
  241. end
  242. end

app/services/flower_giving_service.rb

0.0% lines covered

141 relevant lines. 0 lines covered and 141 lines missed.
    
  1. # 小红花赠送服务
  2. # 负责处理小红花赠送流程,包括配额检查和确认机制
  3. class FlowerGivingService
  4. class << self
  5. # 尝试赠送小红花(带每日配额检查和确认提示)
  6. def give_flower_with_confirmation(giver, recipient, check_in, amount: 1, comment: nil,
  7. flower_type: 'regular', is_anonymous: false, confirmed: false)
  8. # 获取活动和日期
  9. event = check_in.reading_event rescue nil
  10. return { success: false, error: '无法确定活动' } unless event
  11. date = Date.current
  12. # 检查是否是活动日
  13. unless FlowerQuotaService.activity_day?(event, date)
  14. return { success: false, error: '今日不是活动日,无法赠送小红花' }
  15. end
  16. # 检查是否给自己赠送
  17. if giver.id == recipient.id
  18. return { success: false, error: '不能给自己赠送小红花' }
  19. end
  20. # 获取每日配额
  21. quota = FlowerQuotaService.get_daily_quota(giver, event, date)
  22. # 检查配额
  23. unless quota.can_give_flower?(amount)
  24. return {
  25. success: false,
  26. error: '今日小红花配额已用完',
  27. remaining: quota.remaining_flowers,
  28. max: quota.max_flowers,
  29. used: quota.used_flowers
  30. }
  31. end
  32. # 如果未确认,返回确认信息
  33. unless confirmed
  34. return {
  35. success: true,
  36. require_confirmation: true,
  37. confirmation_data: {
  38. giver: giver.as_json_for_api,
  39. recipient: recipient.as_json_for_api,
  40. check_in: {
  41. id: check_in.id,
  42. content: check_in.content.truncate(100),
  43. user: check_in.user.as_json_for_api
  44. },
  45. amount: amount,
  46. comment: comment,
  47. flower_type: flower_type,
  48. is_anonymous: is_anonymous,
  49. date: date,
  50. quota_info: {
  51. used: quota.used_flowers,
  52. max: quota.max_flowers,
  53. remaining: quota.remaining_flowers
  54. },
  55. warning: '赠送成功后无法撤回,请谨慎确认!'
  56. }
  57. }
  58. end
  59. # 使用事务确保数据一致性
  60. ActiveRecord::Base.transaction do
  61. # 扣减配额
  62. unless FlowerQuotaService.use_quota!(giver, event, amount, date)
  63. raise '配额使用失败'
  64. end
  65. # 创建小红花记录
  66. flower = Flower.create!(
  67. giver: giver,
  68. recipient: recipient,
  69. check_in: check_in,
  70. amount: amount,
  71. flower_type: flower_type,
  72. comment: comment,
  73. is_anonymous: is_anonymous
  74. )
  75. # 更新配额的最后赠送时间和今日赠送次数
  76. FlowerQuotaService.record_quota_usage(quota, amount)
  77. # 更新接收者的统计
  78. update_recipient_statistics(recipient, flower)
  79. # 发布小红花赠送事件,解耦通知服务
  80. DomainEventsService.publish('flower.given', {
  81. giver: giver,
  82. recipient: recipient,
  83. flower: flower,
  84. check_in: check_in,
  85. amount: amount,
  86. comment: comment
  87. })
  88. {
  89. success: true,
  90. flower: flower,
  91. remaining_quota: quota.remaining_flowers,
  92. used_today: quota.used_flowers,
  93. message: '小红花赠送成功!此操作无法撤回。'
  94. }
  95. end
  96. rescue => e
  97. Rails.logger.error "小红花赠送失败: #{e.message}"
  98. {
  99. success: false,
  100. error: '小红花赠送失败,请重试',
  101. details: e.message
  102. }
  103. end
  104. # 简化的赠送方法(不要求确认)
  105. def give_flower_simple(giver, recipient, check_in, amount: 1, comment: nil,
  106. flower_type: 'regular', is_anonymous: false)
  107. give_flower_with_confirmation(
  108. giver, recipient, check_in,
  109. amount: amount, comment: comment,
  110. flower_type: flower_type, is_anonymous: is_anonymous,
  111. confirmed: true
  112. )
  113. end
  114. # 批量赠送小红花(管理员功能)
  115. def batch_give_flowers(admin_user, flower_data_list)
  116. results = []
  117. ActiveRecord::Base.transaction do
  118. flower_data_list.each do |flower_data|
  119. result = give_flower_with_confirmation(
  120. flower_data[:giver],
  121. flower_data[:recipient],
  122. flower_data[:check_in],
  123. amount: flower_data[:amount] || 1,
  124. comment: flower_data[:comment],
  125. flower_type: flower_data[:flower_type] || 'regular',
  126. is_anonymous: flower_data[:is_anonymous] || false,
  127. confirmed: true
  128. )
  129. results << result
  130. end
  131. end
  132. {
  133. success: true,
  134. total_processed: results.length,
  135. successful: results.count { |r| r[:success] },
  136. failed: results.count { |r| !r[:success] },
  137. details: results
  138. }
  139. rescue => e
  140. Rails.logger.error "批量赠送小红花失败: #{e.message}"
  141. {
  142. success: false,
  143. error: '批量赠送失败',
  144. details: e.message
  145. }
  146. end
  147. private
  148. # 发送小红花通知已移至事件订阅者中处理
  149. # 这样可以解耦FlowerGivingService和NotificationService的依赖关系
  150. # 更新接收者的统计信息
  151. def update_recipient_statistics(recipient, flower)
  152. enrollment = recipient.event_enrollments
  153. .where(reading_event_id: flower.check_in.reading_event_id)
  154. .first
  155. if enrollment
  156. enrollment.increment!(:flowers_received_count)
  157. enrollment.increment!(:total_flowers_received, flower.amount)
  158. end
  159. end
  160. end
  161. end

app/services/flower_incentive_service.rb

0.0% lines covered

122 relevant lines. 0 lines covered and 122 lines missed.
    
  1. # 小红花激励服务 (重构后 - 适配器模式)
  2. # 作为统一入口,委托给专门的服务类处理具体业务逻辑
  3. # 保持向后兼容性,现有代码无需修改
  4. class FlowerIncentiveService
  5. class << self
  6. # ============================================================================
  7. # 配额管理相关方法 - 委托给 FlowerQuotaService
  8. # ============================================================================
  9. # 检查用户是否可以在活动中赠送小红花(每日配额)
  10. def can_give_flower?(user, event, amount = 1, date = Date.current)
  11. FlowerQuotaService.can_give_flower?(user, event, amount, date)
  12. end
  13. # 获取用户在活动中的每日配额信息
  14. def get_user_daily_quota_info(user, event, date = Date.current)
  15. FlowerQuotaService.get_user_daily_quota_info(user, event, date)
  16. end
  17. # 获取用户在活动中的配额历史
  18. def get_user_quota_history(user, event, days: 7)
  19. FlowerQuotaService.get_user_quota_history(user, event, days: days)
  20. end
  21. # 活动开始时初始化所有参与者的每日配额
  22. def initialize_event_daily_quotas(event, max_flowers: 3, days: nil)
  23. FlowerQuotaService.initialize_event_daily_quotas(event, max_flowers: max_flowers, days: days)
  24. end
  25. # 获取活动的每日配额统计
  26. def get_event_daily_quota_stats(event, date = Date.current)
  27. FlowerQuotaService.get_event_daily_quota_stats(event, date)
  28. end
  29. # 检查配额是否即将用完(提醒功能)
  30. def check_daily_quota_warning(user, event, date = Date.current, threshold: 0.8)
  31. FlowerQuotaService.check_daily_quota_warning(user, event, date, threshold: threshold)
  32. end
  33. # 使用配额(扣减数量)
  34. def use_quota!(user, event, amount, date = Date.current)
  35. FlowerQuotaService.use_quota!(user, event, amount, date)
  36. end
  37. # ============================================================================
  38. # 小红花赠送相关方法 - 委托给 FlowerGivingService
  39. # ============================================================================
  40. # 尝试赠送小红花(带每日配额检查和确认提示)
  41. def give_flower_with_confirmation(giver, recipient, check_in, amount: 1, comment: nil,
  42. flower_type: 'regular', is_anonymous: false, confirmed: false)
  43. FlowerGivingService.give_flower_with_confirmation(
  44. giver, recipient, check_in,
  45. amount: amount, comment: comment,
  46. flower_type: flower_type, is_anonymous: is_anonymous,
  47. confirmed: confirmed
  48. )
  49. end
  50. # 简化的赠送方法(不要求确认)
  51. def give_flower_simple(giver, recipient, check_in, amount: 1, comment: nil,
  52. flower_type: 'regular', is_anonymous: false)
  53. FlowerGivingService.give_flower_simple(
  54. giver, recipient, check_in,
  55. amount: amount, comment: comment,
  56. flower_type: flower_type, is_anonymous: is_anonymous
  57. )
  58. end
  59. # 批量赠送小红花(管理员功能)
  60. def batch_give_flowers(admin_user, flower_data_list)
  61. FlowerGivingService.batch_give_flowers(admin_user, flower_data_list)
  62. end
  63. # ============================================================================
  64. # 证书相关方法 - 委托给 FlowerCertificateService
  65. # ============================================================================
  66. # 活动结束时生成小红花证书
  67. def finalize_event_flower_certificates(event)
  68. FlowerCertificateService.finalize_event_flower_certificates(event)
  69. end
  70. # 获取活动的前三名排行榜
  71. def get_event_top_three(event)
  72. FlowerCertificateService.get_event_top_three(event)
  73. end
  74. # 获取用户的所有小红花证书
  75. def get_user_certificates(user)
  76. FlowerCertificateService.get_user_certificates(user)
  77. end
  78. # 验证证书有效性
  79. def validate_certificate(certificate_id)
  80. FlowerCertificateService.validate_certificate(certificate_id)
  81. end
  82. # 重新生成证书(用于修正错误)
  83. def regenerate_certificate(certificate_id, admin_user)
  84. FlowerCertificateService.regenerate_certificate(certificate_id, admin_user)
  85. end
  86. # 批量生成参与证书
  87. def batch_generate_participation_certificates(event, user_ids = nil)
  88. FlowerCertificateService.batch_generate_participation_certificates(event, user_ids)
  89. end
  90. # ============================================================================
  91. # 向后兼容性方法 - 保持原有接口不变
  92. # ============================================================================
  93. # 旧版本方法名兼容
  94. def can_give_flower_legacy?(user, event, amount = 1)
  95. can_give_flower?(user, event, amount, Date.current)
  96. end
  97. def give_flower_with_quota_legacy(giver, recipient, check_in, amount: 1, comment: nil, flower_type: 'regular', is_anonymous: false)
  98. give_flower_with_confirmation(giver, recipient, check_in,
  99. amount: amount, comment: comment,
  100. flower_type: flower_type, is_anonymous: is_anonymous,
  101. confirmed: false)
  102. end
  103. def get_user_quota_info_legacy(user, event)
  104. get_user_daily_quota_info(user, event, Date.current)
  105. end
  106. def initialize_event_flower_quotas_legacy(event, max_flowers: 3)
  107. initialize_event_daily_quotas(event, max_flowers: max_flowers)
  108. end
  109. # 别名方法,确保现有代码继续工作
  110. alias_method :can_give_flower_old, :can_give_flower_legacy?
  111. alias_method :give_flower_with_quota_old, :give_flower_with_quota_legacy
  112. alias_method :get_user_quota_info_old, :get_user_quota_info_legacy
  113. alias_method :initialize_event_flower_quotas_old, :initialize_event_flower_quotas_legacy
  114. # ============================================================================
  115. # 便捷方法和组合操作
  116. # ============================================================================
  117. # 一键检查用户在活动中的完整状态
  118. def get_user_complete_status(user, event, date = Date.current)
  119. {
  120. quota_info: get_user_daily_quota_info(user, event, date),
  121. can_give_flower: can_give_flower?(user, event, 1, date),
  122. quota_warning: check_daily_quota_warning(user, event, date, threshold: 0.8),
  123. certificates: get_user_certificates(user)
  124. }
  125. end
  126. # 获取活动完整统计信息
  127. def get_event_complete_stats(event, date = Date.current)
  128. {
  129. quota_stats: get_event_daily_quota_stats(event, date),
  130. top_three: event.completed? ? get_event_top_three(event) : nil,
  131. event_status: {
  132. status: event.status,
  133. participants_count: event.participants.count,
  134. is_completed: event.completed?
  135. }
  136. }
  137. end
  138. # 智能赠送建议(基于配额和历史数据)
  139. def get_smart_giving_suggestions(giver, event, limit = 5)
  140. quota_info = get_user_daily_quota_info(giver, event)
  141. return { suggestions: [], message: '今日配额已用完' } unless quota_info[:can_give_more]
  142. # 获取今日可赠送的打卡列表
  143. available_check_ins = CheckIn.joins(:reading_schedule, :user)
  144. .where(reading_schedules: { reading_event: event, date: Date.current })
  145. .where.not(user: giver)
  146. .includes(:user)
  147. .limit(limit)
  148. suggestions = available_check_ins.map do |check_in|
  149. {
  150. check_in: check_in.as_json_for_api(include_user: true),
  151. recommended_flower_type: 'regular',
  152. reason: '今日打卡,值得鼓励'
  153. }
  154. end
  155. {
  156. suggestions: suggestions,
  157. remaining_quota: quota_info[:remaining_flowers],
  158. message: "发现 #{suggestions.count} 个可赠送的打卡"
  159. }
  160. end
  161. end
  162. end

app/services/flower_quota_service.rb

0.0% lines covered

156 relevant lines. 0 lines covered and 156 lines missed.
    
  1. # 小红花配额管理服务
  2. # 负责管理用户的每日小红花配额
  3. class FlowerQuotaService
  4. class << self
  5. # 检查用户是否可以在活动中赠送小红花(每日配额)
  6. def can_give_flower?(user, event, amount = 1, date = Date.current)
  7. return false unless user && event
  8. return false unless event.participants.include?(user)
  9. return false unless event.status.in?(['in_progress', 'approved'])
  10. # 检查是否是活动日
  11. return false unless activity_day?(event, date)
  12. quota = get_daily_quota(user, event, date)
  13. quota.can_give_flower?(amount)
  14. end
  15. # 获取用户在活动中的每日配额信息
  16. def get_user_daily_quota_info(user, event, date = Date.current)
  17. return { error: '用户或活动不存在' } unless user && event
  18. quota = get_daily_quota(user, event, date)
  19. {
  20. user_id: user.id,
  21. event_id: event.id,
  22. date: date,
  23. is_activity_day: activity_day?(event, date),
  24. used_flowers: quota.used_flowers,
  25. max_flowers: quota.max_flowers,
  26. remaining_flowers: quota.remaining_flowers,
  27. usage_percentage: quota.usage_percentage,
  28. can_give_more: quota.can_give_flower?(1),
  29. last_given_at: quota.last_given_at,
  30. give_count_today: quota.give_count_today,
  31. time_remaining: time_remaining_for_quota(date)
  32. }
  33. end
  34. # 获取用户在活动中的配额历史
  35. def get_user_quota_history(user, event, days: 7)
  36. return { error: '用户或活动不存在' } unless user && event
  37. end_date = Date.current
  38. start_date = end_date - days.days + 1
  39. quotas = []
  40. (start_date..end_date).each do |date|
  41. quota_info = get_user_daily_quota_info(user, event, date)
  42. quotas << quota_info
  43. end
  44. {
  45. user: user.as_json_for_api,
  46. event: event.as_json_for_api,
  47. period: "#{start_date} 至 #{end_date}",
  48. quotas: quotas
  49. }
  50. end
  51. # 活动开始时初始化所有参与者的每日配额
  52. def initialize_event_daily_quotas(event, max_flowers: 3, days: nil)
  53. return false unless event
  54. # 默认初始化活动期间的所有日期
  55. days ||= event.days_count
  56. event.participants.each do |user|
  57. event.start_date.upto(event.end_date) do |date|
  58. next if event.weekend_rest && (date.saturday? || date.sunday?)
  59. get_daily_quota(user, event, date, max_flowers)
  60. end
  61. end
  62. true
  63. end
  64. # 获取活动的每日配额统计
  65. def get_event_daily_quota_stats(event, date = Date.current)
  66. return { error: '活动不存在' } unless event
  67. # 获取当日的所有配额记录
  68. quotas = FlowerQuota.joins(:user)
  69. .where(reading_event: event, quota_date: date)
  70. .includes(:user)
  71. total_users = quotas.count
  72. total_used = quotas.sum(:used_flowers)
  73. total_max = quotas.sum(:max_flowers)
  74. users_with_remaining = quotas.select { |q| q.can_give_flower?(1) }.count
  75. users_exhausted = quotas.select { |q| q.remaining_flowers == 0 }.count
  76. {
  77. event: event.as_json_for_api,
  78. date: date,
  79. is_activity_day: activity_day?(event, date),
  80. statistics: {
  81. total_users: total_users,
  82. total_used: total_used,
  83. total_max: total_max,
  84. overall_usage_rate: total_max > 0 ? (total_used.to_f / total_max * 100).round(2) : 0,
  85. users_with_remaining: users_with_remaining,
  86. users_exhausted: users_exhausted
  87. },
  88. top_givers: quotas.order(give_count_today: :desc).limit(10).map do |quota|
  89. {
  90. user: quota.user.as_json_for_api,
  91. used_flowers: quota.used_flowers,
  92. give_count_today: quota.give_count_today,
  93. last_given_at: quota.last_given_at
  94. }
  95. end
  96. }
  97. end
  98. # 检查配额是否即将用完(提醒功能)
  99. def check_daily_quota_warning(user, event, date = Date.current, threshold: 0.8)
  100. return { should_warn: false } unless activity_day?(event, date)
  101. quota = get_daily_quota(user, event, date)
  102. return { should_warn: false } unless quota.max_flowers > 0
  103. usage_ratio = quota.used_flowers.to_f / quota.max_flowers
  104. if usage_ratio >= threshold
  105. {
  106. should_warn: true,
  107. remaining_flowers: quota.remaining_flowers,
  108. usage_percentage: quota.usage_percentage,
  109. message: "今日小红花配额即将用完,还剩余 #{quota.remaining_flowers} 朵",
  110. time_remaining: time_remaining_for_quota(date)
  111. }
  112. else
  113. {
  114. should_warn: false,
  115. remaining_flowers: quota.remaining_flowers,
  116. usage_percentage: quota.usage_percentage,
  117. time_remaining: time_remaining_for_quota(date)
  118. }
  119. end
  120. end
  121. # 使用配额(扣减数量)
  122. def use_quota!(user, event, amount, date = Date.current)
  123. quota = get_daily_quota(user, event, date)
  124. quota.use_flowers!(amount)
  125. end
  126. # 增加配额使用记录
  127. def record_quota_usage(quota, amount)
  128. quota.update!(
  129. last_given_at: Time.current,
  130. give_count_today: quota.give_count_today + amount
  131. )
  132. end
  133. private
  134. # 获取用户在指定日期的配额
  135. def get_daily_quota(user, event, date = Date.current, max_flowers = 3)
  136. FlowerQuota.find_or_initialize_by(
  137. user: user,
  138. reading_event: event,
  139. quota_date: date
  140. ).tap do |quota|
  141. if quota.new_record?
  142. quota.max_flowers = max_flowers
  143. quota.used_flowers = 0
  144. quota.give_count_today = 0
  145. quota.save!
  146. end
  147. end
  148. end
  149. # 检查指定日期是否是活动阅读日
  150. def activity_day?(event, date)
  151. return false unless event.start_date && event.end_date
  152. return false if date < event.start_date || date > event.end_date
  153. # 如果设置周末休息,跳过周末
  154. if event.weekend_rest && (date.saturday? || date.sunday?)
  155. return false
  156. end
  157. true
  158. end
  159. # 计算配额剩余时间
  160. def time_remaining_for_quota(date)
  161. return "已过期" if date < Date.current
  162. return "全天可用" if date > Date.current
  163. # 如果是今天,计算到23:59:59的剩余时间
  164. if date == Date.current
  165. end_of_day = Time.current.end_of_day
  166. remaining_seconds = end_of_day - Time.current
  167. remaining_hours = remaining_seconds / 1.hour
  168. "#{remaining_hours.round(1)} 小时"
  169. else
  170. "全天可用"
  171. end
  172. end
  173. end
  174. end

app/services/flower_statistics_service.rb

0.0% lines covered

210 relevant lines. 0 lines covered and 210 lines missed.
    
  1. # 小红花统计服务
  2. # 提供小红花统计、排行榜、数据分析功能
  3. class FlowerStatisticsService
  4. class << self
  5. # 获取用户小红花统计
  6. def get_user_flower_stats(user, days = 30)
  7. start_date = days.days.ago.to_date
  8. received = Flower.joins(:check_in)
  9. .where(recipient: user)
  10. .where('flowers.created_at >= ?', start_date)
  11. .group(:flower_type)
  12. .sum(:amount)
  13. given = Flower.where(giver: user)
  14. .where('flowers.created_at >= ?', start_date)
  15. .group(:flower_type)
  16. .sum(:amount)
  17. # 按天统计
  18. daily_received = Flower.joins(:check_in)
  19. .where(recipient: user)
  20. .where('flowers.created_at >= ?', start_date)
  21. .group('DATE(flowers.created_at)')
  22. .sum(:amount)
  23. daily_given = Flower.where(giver: user)
  24. .where('flowers.created_at >= ?', start_date)
  25. .group('DATE(flowers.created_at)')
  26. .sum(:amount)
  27. {
  28. period: "#{days}天",
  29. total_received: received.values.sum,
  30. total_given: given.values.sum,
  31. received_by_type: received,
  32. given_by_type: given,
  33. daily_received: daily_received,
  34. daily_given: daily_given,
  35. net_balance: received.values.sum - given.values.sum
  36. }
  37. end
  38. # 获取活动小红花统计
  39. def get_event_flower_stats(event, days = 30)
  40. start_date = days.days.ago.to_date
  41. flowers = Flower.joins(:check_in)
  42. .joins(:reading_schedule)
  43. .where(reading_schedules: { reading_event_id: event.id })
  44. .where('flowers.created_at >= ?', start_date)
  45. # 按天统计
  46. daily_stats = flowers.group('DATE(flowers.created_at)').count
  47. # 按类型统计
  48. type_stats = flowers.group(:flower_type).count
  49. # 参与度统计
  50. participant_stats = flowers.group(:recipient_id).count
  51. # 发放者统计
  52. giver_stats = flowers.group(:giver_id).count
  53. {
  54. period: "#{days}天",
  55. total_flowers: flowers.count,
  56. daily_stats: daily_stats,
  57. type_stats: type_stats,
  58. participant_count: participant_stats.keys.count,
  59. giver_count: giver_stats.keys.count,
  60. avg_flowers_per_participant: participant_stats.values.empty? ? 0 : (participant_stats.values.sum.to_f / participant_stats.count).round(2)
  61. }
  62. end
  63. # 获取小红花排行榜
  64. def get_flower_leaderboard(type = 'received', period = 30, limit = 20)
  65. start_date = period.days.ago.to_date
  66. case type.to_sym
  67. when :received
  68. get_received_leaderboard(start_date, limit)
  69. when :given
  70. get_given_leaderboard(start_date, limit)
  71. when :popular_check_ins
  72. get_popular_check_ins_leaderboard(start_date, limit)
  73. when :generous_givers
  74. get_generous_givers_leaderboard(start_date, limit)
  75. else
  76. get_received_leaderboard(start_date, limit)
  77. end
  78. end
  79. # 获取小红花趋势数据
  80. def get_flower_trends(days = 30)
  81. start_date = days.days.ago.to_date
  82. end_date = Date.current
  83. trends = {}
  84. (start_date..end_date).each do |date|
  85. flowers = Flower.where('DATE(flowers.created_at) = ?', date)
  86. trends[date.to_s] = {
  87. total: flowers.count,
  88. received: flowers.where.not(giver_id: nil).count,
  89. given: flowers.where.not(recipient_id: nil).count
  90. }
  91. end
  92. trends
  93. end
  94. # 获取小红花激励统计
  95. def get_incentive_statistics(days = 30)
  96. start_date = days.days.ago.to_date
  97. end_date = Date.current
  98. # 小红花发放活动参与度
  99. active_events = ReadingEvent.joins(check_ins: :flowers)
  100. .where('reading_events.start_date <= ? AND reading_events.end_date >= ?', end_date, start_date)
  101. .where('flowers.created_at >= ?', start_date)
  102. .distinct
  103. .count
  104. # 活跃用户(发送或接收小红花)
  105. active_users = Flower.where('flowers.created_at >= ?', start_date)
  106. .select('DISTINCT giver_id, recipient_id')
  107. .flat_map { |r| [r.giver_id, r.recipient_id] }
  108. .uniq
  109. .count
  110. # 小红花流转情况
  111. total_flowers = Flower.where('flowers.created_at >= ?', start_date).count
  112. avg_flowers_per_day = total_flowers.to_f / days
  113. {
  114. period: "#{days}天",
  115. active_events: active_events,
  116. active_users: active_users,
  117. total_flowers: total_flowers,
  118. avg_flowers_per_day: avg_flowers_per_day.round(2),
  119. flower_velocity: calculate_flower_velocity(start_date, days)
  120. }
  121. end
  122. # 获取小红花发放建议
  123. def get_flower_suggestions(user, limit = 5)
  124. # 建议给哪些打卡送小红花
  125. suggestions = []
  126. # 1. 最近的高质量打卡但小红花较少的内容
  127. recent_check_ins = CheckIn.joins(:flowers)
  128. .where.not(check_ins: { user_id: user.id })
  129. .where('check_ins.created_at >= ?', 7.days.ago)
  130. .where('check_ins.word_count >= 100')
  131. .group('check_ins.id')
  132. .having('COUNT(flowers.id) < 3')
  133. .order('check_ins.created_at DESC')
  134. .limit(limit)
  135. recent_check_ins.each do |check_in|
  136. suggestions << {
  137. type: 'check_in',
  138. check_in: check_in,
  139. reason: '高质量内容但小红花较少',
  140. priority: 1
  141. }
  142. end
  143. # 2. 活跃的参与者 - 简化版本,只考虑最近打卡的用户
  144. active_participants = User.joins(:check_ins)
  145. .where('check_ins.created_at >= ?', 7.days.ago)
  146. .where.not(users: { id: user.id })
  147. .group('users.id')
  148. .having('COUNT(check_ins.id) >= 3')
  149. .select('users.*')
  150. .order('COUNT(check_ins.id) DESC')
  151. .limit(limit)
  152. active_participants.each do |participant|
  153. suggestions << {
  154. type: 'user',
  155. user: participant,
  156. reason: '活跃参与者',
  157. priority: 2
  158. }
  159. end
  160. suggestions.sort_by { |s| s[:priority] }.first(limit)
  161. end
  162. # 计算小红花流速
  163. def calculate_flower_velocity(start_date, days)
  164. flowers = Flower.where('flowers.created_at >= ?', start_date)
  165. .order(:created_at)
  166. return 0.0 if flowers.count < 2
  167. first_flower = flowers.first
  168. last_flower = flowers.last
  169. time_span = (last_flower.created_at - first_flower.created_at) / 1.hour
  170. return 0.0 if time_span < 1
  171. (flowers.count - 1).to_f / time_span.round(2)
  172. end
  173. private
  174. # 获取接收排行榜
  175. def get_received_leaderboard(start_date, limit)
  176. flower_sums = Flower.where('flowers.created_at >= ?', start_date)
  177. .joins(:recipient)
  178. .group(:recipient_id)
  179. .sum(:amount)
  180. user_ids = flower_sums.keys.sort_by { |user_id| -flower_sums[user_id] }.first(limit)
  181. users = User.where(id: user_ids).index_by(&:id)
  182. user_ids.map do |user_id|
  183. user = users[user_id]
  184. next unless user
  185. # 添加total_flowers属性
  186. user.define_singleton_method(:total_flowers) { flower_sums[user_id] }
  187. user
  188. end.compact
  189. end
  190. # 获取赠送排行榜
  191. def get_given_leaderboard(start_date, limit)
  192. flower_sums = Flower.where('flowers.created_at >= ?', start_date)
  193. .joins(:giver)
  194. .group(:giver_id)
  195. .sum(:amount)
  196. user_ids = flower_sums.keys.sort_by { |user_id| -flower_sums[user_id] }.first(limit)
  197. users = User.where(id: user_ids).index_by(&:id)
  198. user_ids.map do |user_id|
  199. user = users[user_id]
  200. next unless user
  201. # 添加total_flowers属性
  202. user.define_singleton_method(:total_flowers) { flower_sums[user_id] }
  203. user
  204. end.compact
  205. end
  206. # 获取热门打卡排行榜
  207. def get_popular_check_ins_leaderboard(start_date, limit)
  208. flower_counts = Flower.where('flowers.created_at >= ?', start_date)
  209. .group(:check_in_id)
  210. .count
  211. check_in_ids = flower_counts.keys.sort_by { |check_in_id| -flower_counts[check_in_id] }.first(limit)
  212. check_ins = CheckIn.where(id: check_in_ids).includes(:user).index_by(&:id)
  213. check_in_ids.map do |check_in_id|
  214. check_in = check_ins[check_in_id]
  215. next unless check_in
  216. # 添加flower_count属性
  217. check_in.define_singleton_method(:flower_count) { flower_counts[check_in_id] }
  218. check_in
  219. end.compact
  220. end
  221. # 获取慷慨赠送者排行榜
  222. def get_generous_givers_leaderboard(start_date, limit)
  223. flower_counts = Flower.where('flowers.created_at >= ?', start_date)
  224. .where.not(is_anonymous: true)
  225. .group(:giver_id)
  226. .count
  227. user_ids = flower_counts.keys.sort_by { |user_id| -flower_counts[user_id] }.first(limit)
  228. users = User.where(id: user_ids).index_by(&:id)
  229. user_ids.map do |user_id|
  230. user = users[user_id]
  231. next unless user
  232. # 添加giving_count属性
  233. user.define_singleton_method(:giving_count) { flower_counts[user_id] }
  234. user
  235. end.compact
  236. end
  237. end
  238. end

app/services/global_error_handler_service.rb

0.0% lines covered

348 relevant lines. 0 lines covered and 348 lines missed.
    
  1. # frozen_string_literal: true
  2. # GlobalErrorHandlerService - 全局错误处理服务
  3. # 提供统一的错误处理、日志记录和用户友好的错误响应
  4. class GlobalErrorHandlerService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :exception, :context, :user, :request_id
  7. def initialize(exception:, context: {}, user: nil, request_id: nil)
  8. super()
  9. @exception = exception
  10. @context = context
  11. @user = user
  12. @request_id = request_id || SecureRandom.uuid
  13. end
  14. def call
  15. handle_errors do
  16. log_error
  17. determine_error_response
  18. end
  19. self
  20. end
  21. # 类方法:处理控制器异常
  22. def self.handle_controller_exception(exception, controller, action = nil)
  23. user = controller.respond_to?(:current_user) ? controller.current_user : nil
  24. request_id = controller.request&.request_id
  25. new(
  26. exception: exception,
  27. context: {
  28. controller: controller.class.name,
  29. action: action,
  30. method: controller.request&.request_method,
  31. path: controller.request&.path,
  32. ip: controller.request&.remote_ip,
  33. user_agent: controller.request&.user_agent
  34. },
  35. user: user,
  36. request_id: request_id
  37. ).call
  38. end
  39. # 类方法:处理服务异常
  40. def self.handle_service_exception(exception, service_name, action = nil)
  41. new(
  42. exception: exception,
  43. context: {
  44. service: service_name,
  45. action: action
  46. },
  47. user: nil,
  48. request_id: SecureRandom.uuid
  49. ).call
  50. end
  51. def error_response
  52. @error_response
  53. end
  54. def error_code
  55. @error_code ||= determine_error_code
  56. end
  57. def error_message
  58. @error_message ||= determine_error_message
  59. end
  60. def should_retry?
  61. @should_retry ||= determine_retry_eligibility
  62. end
  63. def severity
  64. @severity ||= determine_severity
  65. end
  66. private
  67. def log_error
  68. return unless should_log_error?
  69. # 基本错误信息
  70. Rails.logger.error(
  71. "[#{severity.upcase}] #{exception.class.name}: #{exception.message}",
  72. {
  73. request_id: request_id,
  74. user_id: user&.id,
  75. user_role: user&.role_as_string,
  76. context: context,
  77. exception_class: exception.class.name,
  78. exception_message: exception.message,
  79. backtrace: exception.backtrace&.first(10)
  80. }
  81. )
  82. # 详细错误信息(仅在开发环境)
  83. if Rails.env.development?
  84. Rails.logger.debug(
  85. "完整错误堆栈:",
  86. exception.backtrace&.join("\n")
  87. )
  88. end
  89. # 发送错误通知(生产环境)
  90. send_error_notification if should_send_notification?
  91. end
  92. def determine_error_response
  93. case exception
  94. when ActionController::ParameterMissing, ActionController::BadRequest
  95. build_validation_error_response
  96. when ActiveRecord::RecordNotFound
  97. build_not_found_error_response
  98. when ActiveRecord::RecordInvalid
  99. build_validation_error_response
  100. when ActionDispatch::Http::Parameters::InvalidParameter
  101. build_parameter_error_response
  102. when JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature
  103. build_authentication_error_response
  104. when Timeout::Error, ActiveRecord::StatementInvalid
  105. build_system_error_response
  106. else
  107. build_general_error_response
  108. end
  109. end
  110. def determine_error_code
  111. case exception
  112. when ActionController::ParameterMissing
  113. 'MISSING_PARAMETER'
  114. when ActionController::BadRequest
  115. 'INVALID_REQUEST'
  116. when ActiveRecord::RecordNotFound
  117. 'RESOURCE_NOT_FOUND'
  118. when ActiveRecord::RecordInvalid
  119. 'VALIDATION_ERROR'
  120. when ActionDispatch::Http::Parameters::InvalidParameter
  121. 'INVALID_PARAMETER'
  122. when JWT::DecodeError, JWT::VerificationError
  123. 'INVALID_TOKEN'
  124. when JWT::ExpiredSignature
  125. 'TOKEN_EXPIRED'
  126. when Timeout::Error
  127. 'TIMEOUT_ERROR'
  128. when ActiveRecord::StatementInvalid
  129. 'DATABASE_ERROR'
  130. else
  131. 'INTERNAL_ERROR'
  132. end
  133. end
  134. def determine_error_message
  135. case exception
  136. when ActionController::ParameterMissing
  137. "缺少必需的参数: #{exception.param}"
  138. when ActionController::BadRequest
  139. "请求格式错误"
  140. when ActiveRecord::RecordNotFound
  141. "请求的资源不存在"
  142. when ActiveRecord::RecordInvalid
  143. "数据验证失败: #{format_validation_errors}"
  144. when ActionDispatch::Http::Parameters::InvalidParameter
  145. "参数格式错误: #{exception.message}"
  146. when JWT::DecodeError, JWT::VerificationError
  147. "认证令牌无效"
  148. when JWT::ExpiredSignature
  149. "认证令牌已过期"
  150. when Timeout::Error
  151. "请求超时,请稍后重试"
  152. when ActiveRecord::StatementInvalid
  153. "数据库操作失败"
  154. else
  155. "系统繁忙,请稍后重试"
  156. end
  157. end
  158. def determine_retry_eligibility
  159. # 可以重试的错误类型
  160. retryable_errors = [
  161. Timeout::Error,
  162. ActiveRecord::StatementInvalid,
  163. Net::TimeoutError,
  164. Net::ReadTimeout,
  165. Net::OpenTimeout
  166. ]
  167. retryable_errors.include?(exception.class) && !should_fail_fast?
  168. end
  169. def determine_severity
  170. case exception
  171. when ActionController::ParameterMissing, ActiveRecord::RecordInvalid
  172. :low
  173. when ActionDispatch::Http::Parameters::InvalidParameter, JWT::DecodeError
  174. :medium
  175. when Timeout::Error, ActiveRecord::StatementInvalid
  176. :high
  177. else
  178. :critical
  179. end
  180. end
  181. def build_validation_error_response
  182. errors = extract_validation_errors
  183. {
  184. error: error_message,
  185. error_code: error_code,
  186. error_type: 'validation_error',
  187. errors: errors,
  188. request_id: request_id,
  189. timestamp: Time.current.iso8601,
  190. details: {
  191. context: context,
  192. fix_suggestions: generate_fix_suggestions
  193. }
  194. }
  195. end
  196. def build_not_found_error_response
  197. {
  198. error: error_message,
  199. error_code: error_code,
  200. error_type: 'not_found',
  201. request_id: request_id,
  202. timestamp: Time.current.iso8601,
  203. details: {
  204. context: context,
  205. suggestions: [
  206. '请检查资源ID是否正确',
  207. '确认资源是否存在且未被删除'
  208. ]
  209. }
  210. }
  211. end
  212. def build_parameter_error_response
  213. {
  214. error: error_message,
  215. error_code: error_code,
  216. error_type: 'parameter_error',
  217. request_id: request_id,
  218. timestamp: Time.current.iso8601,
  219. details: {
  220. context: context,
  221. parameter_info: extract_parameter_info,
  222. suggestions: [
  223. '请检查请求参数格式',
  224. '参考API文档确认参数要求'
  225. ]
  226. }
  227. }
  228. end
  229. def build_authentication_error_response
  230. {
  231. error: error_message,
  232. error_code: error_code,
  233. error_type: 'authentication_error',
  234. request_id: request_id,
  235. timestamp: Time.current.iso8601,
  236. details: {
  237. context: context,
  238. auth_info: extract_auth_info,
  239. suggestions: [
  240. '请重新登录获取有效令牌',
  241. '检查令牌是否完整且未过期'
  242. ]
  243. }
  244. }
  245. end
  246. def build_system_error_response
  247. {
  248. error: error_message,
  249. error_code: error_code,
  250. error_type: 'system_error',
  251. request_id: request_id,
  252. timestamp: Time.current.iso8601,
  253. details: {
  254. context: context,
  255. severity: severity,
  256. suggestions: [
  257. '请稍后重试',
  258. '如问题持续存在,请联系技术支持'
  259. ]
  260. }
  261. }
  262. end
  263. def build_general_error_response
  264. {
  265. error: error_message,
  266. error_code: error_code,
  267. error_type: 'general_error',
  268. request_id: request_id,
  269. timestamp: Time.current.iso8601,
  270. details: {
  271. context: context,
  272. exception_class: exception.class.name,
  273. severity: severity,
  274. suggestions: [
  275. '请检查请求格式并重试',
  276. '如问题持续存在,请联系技术支持'
  277. ]
  278. }
  279. }
  280. end
  281. def extract_validation_errors
  282. if exception.is_a?(ActiveRecord::RecordInvalid)
  283. exception.record.errors.full_messages
  284. elsif exception.is_a?(ActionController::ParameterMissing)
  285. [exception.message]
  286. elsif exception.respond_to?(:errors)
  287. exception.errors.full_messages
  288. else
  289. [exception.message]
  290. end
  291. end
  292. def extract_parameter_info
  293. return {} unless context
  294. {
  295. method: context[:method],
  296. path: context[:path],
  297. controller: context[:controller],
  298. action: context[:action]
  299. }
  300. end
  301. def extract_auth_info
  302. return {} unless user
  303. {
  304. user_id: user.id,
  305. user_role: user.role_as_string,
  306. user_nickname: user.nickname
  307. }
  308. end
  309. def generate_fix_suggestions
  310. suggestions = []
  311. case exception
  312. when ActiveRecord::RecordInvalid
  313. suggestions << "请检查必填字段是否完整"
  314. suggestions << "确认数据格式是否正确"
  315. when ActionController::ParameterMissing
  316. suggestions << "请添加缺少的必需参数"
  317. when ActionController::BadRequest
  318. suggestions << "请检查请求格式和参数"
  319. end
  320. suggestions
  321. end
  322. def should_log_error?
  323. # 不记录的错误类型(避免日志噪音)
  324. non_loggable_errors = [
  325. 'MISSING_PARAMETER',
  326. 'INVALID_REQUEST'
  327. ]
  328. return false if non_loggable_errors.include?(error_code)
  329. true
  330. end
  331. def should_send_notification?
  332. return false unless Rails.env.production?
  333. return false unless severity == :critical
  334. return false if exception.is_a?(ActiveRecord::RecordNotFound)
  335. return false if exception.is_a?(ActionController::ParameterMissing)
  336. true
  337. end
  338. def send_error_notification
  339. # 这里可以集成通知系统,如Slack、邮件等
  340. # 示例实现:
  341. begin
  342. ErrorNotificationService.notify(
  343. error: exception,
  344. context: context,
  345. user: user,
  346. request_id: request_id
  347. )
  348. rescue => e
  349. Rails.logger.error "发送错误通知失败: #{e.message}"
  350. end
  351. end
  352. def should_fail_fast?
  353. # 需要快速失败的错误类型
  354. fail_fast_errors = [
  355. 'MISSING_AUTH_HEADER',
  356. 'INVALID_TOKEN_FORMAT',
  357. 'TOKEN_EXPIRED'
  358. ]
  359. fail_fast_errors.include?(error_code)
  360. end
  361. end

app/services/leader_assignment_service.rb

33.33% lines covered

189 relevant lines. 63 lines covered and 126 lines missed.
    
  1. # frozen_string_literal: true
  2. # LeaderAssignmentService - 领读人分配管理服务
  3. # 负责多种分配算法、权限管理、工作统计、补位机制等业务逻辑
  4. 1 class LeaderAssignmentService < ApplicationService
  5. 1 attr_reader :event, :user, :schedule, :action, :assignment_options
  6. 1 def initialize(event:, user: nil, schedule: nil, action: nil, assignment_options: {})
  7. 3 super()
  8. 3 @event = event
  9. 3 @user = user
  10. 3 @schedule = schedule
  11. 3 @action = action
  12. 3 @assignment_options = assignment_options.with_indifferent_access
  13. end
  14. # 主要调用方法
  15. 1 def call
  16. 3 handle_errors do
  17. 3 case action
  18. when :claim_leadership
  19. claim_leadership
  20. when :auto_assign
  21. 3 auto_assign_leaders
  22. when :backup_assign
  23. backup_assignment
  24. when :reassign
  25. reassign_leader
  26. when :get_statistics
  27. get_assignment_statistics
  28. when :check_permissions
  29. check_leader_permissions
  30. else
  31. failure!("不支持的操作: #{action}")
  32. end
  33. end
  34. end
  35. # 类方法:自由报名领读
  36. 1 def self.claim_leadership!(event, user, schedule)
  37. new(event: event, user: user, schedule: schedule, action: :claim_leadership).call
  38. end
  39. # 类方法:自动分配领读人
  40. 1 def self.auto_assign_leaders!(event, assignment_type: nil, options: {})
  41. 3 new(event: event, action: :auto_assign, assignment_options: { assignment_type: assignment_type }.merge(options)).call
  42. end
  43. # 类方法:补位分配
  44. 1 def self.backup_assignment!(event, schedule, backup_leader)
  45. new(event: event, schedule: schedule, user: backup_leader, action: :backup_assign).call
  46. end
  47. # 类方法:重新分配领读人
  48. 1 def self.reassign_leader!(event, schedule, new_leader)
  49. new(event: event, schedule: schedule, user: new_leader, action: :reassign).call
  50. end
  51. # 类方法:获取分配统计
  52. 1 def self.assignment_statistics(event)
  53. new(event: event, action: :get_statistics).call
  54. end
  55. # 类方法:检查领读权限
  56. 1 def self.check_permissions(event, user, schedule = nil)
  57. new(event: event, user: user, schedule: schedule, action: :check_permissions).call
  58. end
  59. 1 private
  60. # 自由报名领读
  61. 1 def claim_leadership
  62. # 检查是否是自由报名模式
  63. unless event.leader_assignment_type == 'voluntary'
  64. return failure!("该活动不支持自由报名领读")
  65. end
  66. # 检查是否已报名该活动
  67. unless user.enrollments.exists?(reading_event: event)
  68. return failure!("请先报名该活动")
  69. end
  70. # 检查是否已有领读人
  71. if schedule.daily_leader.present?
  72. return failure!("该日已有领读人")
  73. end
  74. # 检查领读次数限制
  75. leadership_count = event.reading_schedules.where(daily_leader: user).count
  76. if leadership_count >= 3
  77. return failure!("领读次数已达上限")
  78. end
  79. # 分配领读人
  80. schedule.update!(daily_leader: user)
  81. success!({
  82. message: "领读报名成功",
  83. schedule_data: {
  84. id: schedule.id,
  85. day_number: schedule.day_number,
  86. date: schedule.date,
  87. leader: {
  88. id: user.id,
  89. nickname: user.nickname,
  90. avatar_url: user.avatar_url
  91. }
  92. }
  93. })
  94. end
  95. # 自动分配领读人(支持多种算法)
  96. 1 def auto_assign_leaders
  97. 3 return failure!("活动未审批或没有日程安排") unless event.approved? && event.reading_schedules.any?
  98. 3 assignment_type = @assignment_options[:assignment_type] || event.leader_assignment_type
  99. 3 case assignment_type.to_sym
  100. when :random
  101. 1 assign_random_leaders!
  102. when :balanced
  103. assign_balanced_leaders!
  104. when :rotation
  105. assign_rotation_leaders!
  106. when :voluntary
  107. 2 assign_voluntary_leaders!
  108. else
  109. return failure!("不支持的分配方式: #{assignment_type}")
  110. end
  111. 2 success!({
  112. message: "领读人分配完成",
  113. assignment_type: assignment_type,
  114. assigned_count: event.reading_schedules.where.not(daily_leader: nil).count
  115. })
  116. end
  117. # 随机分配领读人算法
  118. 1 def assign_random_leaders!
  119. 1 participants = get_available_participants
  120. return failure!("没有参与者可供分配") if participants.empty?
  121. schedules = event.reading_schedules.order(:day_number)
  122. schedules.each_with_index do |schedule, index|
  123. leader = participants[index % participants.length]
  124. schedule.update!(daily_leader: leader)
  125. end
  126. end
  127. # 平衡分配算法(基于历史工作量)
  128. 1 def assign_balanced_leaders!
  129. participants = get_available_participants
  130. return failure!("没有参与者可供分配") if participants.empty?
  131. # 计算历史工作量
  132. leader_workloads = calculate_leader_workloads(participants)
  133. schedules = event.reading_schedules.order(:day_number)
  134. schedules.each do |schedule|
  135. # 选择工作量最小的参与者
  136. least_busy_leader = leader_workloads.min_by { |_, workload| workload }.first
  137. schedule.update!(daily_leader: least_busy_leader)
  138. # 更新工作量
  139. leader_workloads[least_busy_leader] += 1
  140. end
  141. end
  142. # 轮换分配算法(确保每个人都能领读,避免连续领读)
  143. 1 def assign_rotation_leaders!
  144. participants = get_available_participants
  145. return failure!("没有参与者可供分配") if participants.empty?
  146. schedules = event.reading_schedules.order(:day_number)
  147. rotation_queue = participants.dup
  148. last_leader = nil
  149. schedules.each do |schedule|
  150. # 避免连续分配给同一个人
  151. if rotation_queue.first == last_leader && rotation_queue.size > 1
  152. rotation_queue.rotate!
  153. end
  154. leader = rotation_queue.first
  155. schedule.update!(daily_leader: leader)
  156. last_leader = leader
  157. # 将领过的人移到队列末尾
  158. rotation_queue.rotate!
  159. end
  160. end
  161. # 自愿分配算法(基于自愿报名)
  162. 1 def assign_voluntary_leaders!
  163. 2 volunteer_assignments = @assignment_options[:volunteer_assignments] || {}
  164. 2 schedules = event.reading_schedules.order(:day_number)
  165. 2 assigned_count = 0
  166. 2 schedules.each do |schedule|
  167. 4 if volunteer_assignments[schedule.id]
  168. user_id = volunteer_assignments[schedule.id]
  169. user = User.find_by(id: user_id)
  170. if user && can_be_leader?(user)
  171. schedule.update!(daily_leader: user)
  172. assigned_count += 1
  173. end
  174. end
  175. end
  176. 2 success!({
  177. message: "自愿分配完成",
  178. assigned_count: assigned_count
  179. })
  180. end
  181. # 补位分配机制
  182. 1 def backup_assignment
  183. return failure!("补位需要指定日程和补位人") unless schedule && user
  184. # 检查补位权限
  185. unless event.leader == user
  186. return failure!("只有活动创建者可以进行补位分配")
  187. end
  188. # 检查日程是否需要补位
  189. unless schedule_needs_backup?(schedule)
  190. return failure!("该日程不需要补位")
  191. end
  192. ActiveRecord::Base.transaction do
  193. schedule.update!(daily_leader: user)
  194. # 记录补位操作
  195. log_backup_assignment(schedule, user)
  196. end
  197. success!({
  198. message: "补位分配成功",
  199. schedule: schedule_info(schedule),
  200. backup_leader: user_info(user)
  201. })
  202. end
  203. # 重新分配领读人
  204. 1 def reassign_leader
  205. return failure!("需要指定日程和新领读人") unless schedule && user
  206. # 检查权限
  207. unless can_reassign_leader?(user)
  208. return failure!("权限不足")
  209. end
  210. old_leader = schedule.daily_leader
  211. ActiveRecord::Base.transaction do
  212. schedule.update!(daily_leader: user)
  213. # 记录重新分配操作
  214. log_reassignment(schedule, old_leader, user)
  215. end
  216. success!({
  217. message: "领读人重新分配成功",
  218. schedule: schedule_info(schedule),
  219. old_leader: old_leader ? user_info(old_leader) : nil,
  220. new_leader: user_info(user)
  221. })
  222. end
  223. # 获取分配统计信息
  224. 1 def get_assignment_statistics
  225. schedules = event.reading_schedules.includes(:daily_leader, :daily_leading)
  226. total_schedules = schedules.count
  227. assigned_schedules = schedules.where.not(daily_leader: nil).count
  228. leaders = schedules.where.not(daily_leader: nil).pluck(:daily_leader_id).uniq
  229. stats = {
  230. total_schedules: total_schedules,
  231. assigned_schedules: assigned_schedules,
  232. unassigned_schedules: total_schedules - assigned_schedules,
  233. unique_leaders: leaders.count,
  234. assignment_rate: total_schedules > 0 ? (assigned_schedules.to_f / total_schedules * 100).round(2) : 0,
  235. leader_workload: calculate_leader_workload_statistics(schedules),
  236. backup_needed: backup_needed_schedules.size,
  237. content_completion_rate: calculate_content_completion_rate(schedules)
  238. }
  239. success!(stats)
  240. end
  241. # 检查领读权限
  242. 1 def check_leader_permissions
  243. return success!({ can_view: false, message: "用户不存在" }) unless user
  244. return success!({ can_view: false, message: "用户未报名活动" }) unless user.enrolled?(event)
  245. permissions = {
  246. can_view: true,
  247. can_claim_leadership: can_claim_leadership?,
  248. can_be_assigned: can_be_assigned_as_leader?,
  249. can_backup: can_backup_assignment?,
  250. current_schedules: user_leading_schedules,
  251. permission_window: get_permission_window_info
  252. }
  253. success!(permissions)
  254. end
  255. 1 private
  256. # 辅助方法
  257. 1 def get_available_participants
  258. 1 event.enrollments.includes(:user).where(role: :participant).map(&:user).compact
  259. end
  260. 1 def can_be_leader?(user)
  261. return false unless user
  262. return false unless user.enrolled?(event)
  263. return false unless event.enrollments.find_by(user: user)&.participant?
  264. true
  265. end
  266. 1 def can_claim_leadership?
  267. return false unless event.leader_assignment_type == 'voluntary'
  268. return false unless schedule
  269. return false if schedule.daily_leader.present?
  270. # 检查领读次数限制
  271. leadership_count = event.reading_schedules.where(daily_leader: user).count
  272. leadership_count < (@assignment_options[:max_leadership_count] || 3)
  273. end
  274. 1 def can_be_assigned_as_leader?
  275. can_be_leader?(user) && event.in_progress?
  276. end
  277. 1 def can_backup_assignment?
  278. event.leader == user
  279. end
  280. 1 def can_reassign_leader?(user)
  281. # 活动创建者可以重新分配
  282. return true if event.leader == user
  283. # 或者在权限窗口内的领读人
  284. event.current_daily_leader?(user, schedule)
  285. end
  286. 1 def schedule_needs_backup?(schedule)
  287. # 检查是否缺少领读人
  288. return true unless schedule.daily_leader.present?
  289. # 检查是否缺少领读内容
  290. if schedule.daily_leader.present? && !schedule.daily_leading.present?
  291. return true
  292. end
  293. # 检查是否缺少小红花(如果有打卡的话)
  294. if schedule.date <= Date.today && schedule.check_ins.any? && schedule.flowers.empty?
  295. return true
  296. end
  297. false
  298. end
  299. 1 def calculate_leader_workloads(participants)
  300. workloads = participants.index_by(&:id).transform_values { 0 }
  301. # 可以扩展为查询历史工作量
  302. # 目前简化处理,所有参与者初始工作量为0
  303. workloads
  304. end
  305. 1 def calculate_leader_workload_statistics(schedules)
  306. workload = {}
  307. schedules.where.not(daily_leader: nil).each do |schedule|
  308. leader_id = schedule.daily_leader_id
  309. workload[leader_id] ||= {
  310. nickname: schedule.daily_leader.nickname,
  311. assigned_count: 0,
  312. content_completed: 0,
  313. flowers_given: 0
  314. }
  315. workload[leader_id][:assigned_count] += 1
  316. workload[leader_id][:content_completed] += 1 if schedule.daily_leading.present?
  317. workload[leader_id][:flowers_given] += schedule.flowers.count
  318. end
  319. workload.values
  320. end
  321. 1 def calculate_content_completion_rate(schedules)
  322. return 0 if schedules.empty?
  323. completed_count = schedules.joins(:daily_leading).count
  324. (completed_count.to_f / schedules.count * 100).round(2)
  325. end
  326. 1 def backup_needed_schedules
  327. event.schedules_need_backup || []
  328. end
  329. 1 def user_leading_schedules
  330. return [] unless user
  331. event.reading_schedules
  332. .where(daily_leader: user)
  333. .includes(:daily_leading, :flowers, :check_ins)
  334. .map do |schedule|
  335. {
  336. id: schedule.id,
  337. day_number: schedule.day_number,
  338. date: schedule.date,
  339. has_content: schedule.daily_leading.present?,
  340. flowers_count: schedule.flowers.count,
  341. check_ins_count: schedule.check_ins.count
  342. }
  343. end
  344. end
  345. 1 def get_permission_window_info
  346. return {} unless user && schedule
  347. {
  348. can_publish_content: event.can_publish_leading_content?(user, schedule),
  349. can_give_flowers: event.can_give_flowers?(user, schedule),
  350. permission_deadline: schedule.date + 1.day
  351. }
  352. end
  353. 1 def schedule_info(schedule)
  354. {
  355. id: schedule.id,
  356. day_number: schedule.day_number,
  357. date: schedule.date,
  358. reading_progress: schedule.reading_progress
  359. }
  360. end
  361. 1 def user_info(user)
  362. {
  363. id: user.id,
  364. nickname: user.nickname,
  365. avatar_url: user.avatar_url
  366. }
  367. end
  368. 1 def log_backup_assignment(schedule, backup_leader)
  369. Rails.logger.info "补位分配: 活动 #{event.id}, 日程 #{schedule.id}, 补位人 #{backup_leader.nickname}"
  370. end
  371. 1 def log_reassignment(schedule, old_leader, new_leader)
  372. Rails.logger.info "重新分配: 活动 #{event.id}, 日程 #{schedule.id}, 原领读人 #{old_leader&.nickname}, 新领读人 #{new_leader.nickname}"
  373. end
  374. end

app/services/moderation_notification_service.rb

0.0% lines covered

238 relevant lines. 0 lines covered and 238 lines missed.
    
  1. # frozen_string_literal: true
  2. # ModerationNotificationService - 内容审核通知服务
  3. # 专门负责内容审核相关的通知管理
  4. class ModerationNotificationService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :report, :notification_type, :options
  7. def initialize(report:, notification_type:, options: {})
  8. super()
  9. @report = report
  10. @notification_type = notification_type
  11. @options = options.with_indifferent_access
  12. end
  13. # 发送通知
  14. def call
  15. handle_errors do
  16. validate_notification_params
  17. send_notifications
  18. log_notification_activity
  19. end
  20. self
  21. end
  22. # 类方法:通知管理员有新举报
  23. def self.notify_admins_of_new_report(report)
  24. new(
  25. report: report,
  26. notification_type: :new_report
  27. ).call
  28. end
  29. # 类方法:通知举报人状态更新
  30. def self.notify_reporter_of_status_change(report)
  31. new(
  32. report: report,
  33. notification_type: :status_change
  34. ).call
  35. end
  36. # 类方法:通知内容作者
  37. def self.notify_content_author(report, action_taken:)
  38. new(
  39. report: report,
  40. notification_type: :content_action,
  41. options: { action_taken: action_taken }
  42. ).call
  43. end
  44. # 类方法:发送每日审核摘要
  45. def self.send_daily_summary(date = Date.current)
  46. reports = ContentReport.where(created_at: date.all_day)
  47. new(
  48. report: nil,
  49. notification_type: :daily_summary,
  50. options: { date: date, reports: reports }
  51. ).call
  52. end
  53. private
  54. # 验证通知参数
  55. def validate_notification_params
  56. case notification_type
  57. when :new_report, :status_change, :content_action
  58. return failure!("举报不能为空") unless report
  59. return failure!("举报不存在") unless report.persisted?
  60. when :daily_summary
  61. # 这些通知类型不需要具体的report实例
  62. else
  63. return failure!("无效的通知类型")
  64. end
  65. true
  66. end
  67. # 发送通知
  68. def send_notifications
  69. case notification_type
  70. when :new_report
  71. notify_admins_new_report
  72. when :status_change
  73. notify_reporter_status_change
  74. when :content_action
  75. notify_content_author_action
  76. when :daily_summary
  77. send_daily_summary_notifications
  78. end
  79. true
  80. end
  81. # 通知管理员有新举报
  82. def notify_admins_new_report
  83. return unless Rails.env.production? || options[:force_notification]
  84. admins = get_admin_users
  85. return if admins.empty?
  86. admins.each do |admin|
  87. send_notification_to_admin(admin, :new_report, {
  88. report_id: report.id,
  89. reason: report.reason,
  90. reporter: report.user&.nickname,
  91. content_preview: get_content_preview
  92. })
  93. end
  94. end
  95. # 通知举报人状态更新
  96. def notify_reporter_status_change
  97. return unless Rails.env.production? || options[:force_notification]
  98. return unless report.user
  99. send_notification_to_user(report.user, :report_status_change, {
  100. report_id: report.id,
  101. status: report.status,
  102. admin_notes: report.notes,
  103. processed_at: report.updated_at
  104. })
  105. end
  106. # 通知内容作者
  107. def notify_content_author_action
  108. return unless Rails.env.production? || options[:force_notification]
  109. return unless report&.target_content&.user
  110. content_author = report.target_content.user
  111. return if content_author == report.user # 不通知自己举报自己的情况
  112. send_notification_to_user(content_author, :content_moderation_action, {
  113. content_id: report.target_content.id,
  114. content_type: report.target_content.class.name,
  115. action_taken: options[:action_taken],
  116. reason: report.reason,
  117. moderator: report.admin&.nickname
  118. })
  119. end
  120. # 发送每日审核摘要
  121. def send_daily_summary_notifications
  122. return unless Rails.env.production? || options[:force_notification]
  123. date = options[:date]
  124. reports = options[:reports]
  125. return if reports.empty?
  126. admins = get_admin_users
  127. return if admins.empty?
  128. summary_data = generate_daily_summary(date, reports)
  129. admins.each do |admin|
  130. send_notification_to_admin(admin, :daily_summary, summary_data)
  131. end
  132. end
  133. # 发送通知给管理员
  134. def send_notification_to_admin(admin, type, data)
  135. # 这里可以集成多种通知方式
  136. send_in_app_notification(admin, type, data)
  137. send_email_notification(admin, type, data) if should_send_email?(admin, type)
  138. send_push_notification(admin, type, data) if should_send_push?(admin, type)
  139. end
  140. # 发送通知给用户
  141. def send_notification_to_user(user, type, data)
  142. # 用户通知主要使用应用内通知
  143. send_in_app_notification(user, type, data)
  144. send_email_notification(user, type, data) if should_send_email_to_user?(user, type)
  145. end
  146. # 发送应用内通知
  147. def send_in_app_notification(user, type, data)
  148. notification_data = build_notification_data(type, data)
  149. # 这里应该调用通知服务创建应用内通知
  150. # NotificationService.create_notification(user, notification_data)
  151. Rails.logger.info "In-app notification created for #{user.nickname}: #{type} - #{notification_data[:title]}"
  152. end
  153. # 发送邮件通知
  154. def send_email_notification(user, type, data)
  155. return unless user.respond_to?(:email) && user.email.present?
  156. # 这里应该调用邮件服务发送邮件
  157. # EmailService.send_moderation_notification(user, type, data)
  158. Rails.logger.info "Email notification sent to #{user.email}: #{type}"
  159. end
  160. # 发送推送通知
  161. def send_push_notification(user, type, data)
  162. # 这里应该调用推送服务发送推送
  163. # PushService.send_notification(user, build_push_data(type, data))
  164. Rails.logger.info "Push notification sent to #{user.nickname}: #{type}"
  165. end
  166. # 构建通知数据
  167. def build_notification_data(type, data)
  168. case type
  169. when :new_report
  170. {
  171. title: '新内容举报',
  172. message: "#{data[:reporter]} 举报了内容:#{data[:reason]}",
  173. url: "/admin/content_reports/#{data[:report_id]}",
  174. priority: 'high'
  175. }
  176. when :report_status_change
  177. {
  178. title: '举报状态更新',
  179. message: "您的举报已#{data[:status]},管理员备注:#{data[:admin_notes]}",
  180. url: "/user/reports/#{data[:report_id]}"
  181. }
  182. when :content_moderation_action
  183. action_text = data[:action_taken] == 'hidden' ? '已被隐藏' : '已被处理'
  184. {
  185. title: '内容审核通知',
  186. message: "您的内容#{action_text},原因:#{data[:reason]}",
  187. url: "/user/content/#{data[:content_id]}"
  188. }
  189. when :daily_summary
  190. {
  191. title: '每日审核摘要',
  192. message: "昨日共收到#{data[:total_reports]}个举报,已处理#{data[:processed_reports]}个",
  193. url: "/admin/analytics/content_moderation"
  194. }
  195. else
  196. {
  197. title: '内容审核通知',
  198. message: '您有新的内容审核相关信息'
  199. }
  200. end
  201. end
  202. # 生成每日摘要数据
  203. def generate_daily_summary(date, reports)
  204. {
  205. date: date,
  206. total_reports: reports.count,
  207. pending_reports: reports.pending.count,
  208. processed_reports: reports.where.not(status: :pending).count,
  209. by_reason: reports.group(:reason).count,
  210. high_priority_reports: reports.where(reason: %w[sensitive_words harassment]).count
  211. }
  212. end
  213. # 获取管理员用户
  214. def get_admin_users
  215. User.where(role: 1).or(User.where(role: 'admin'))
  216. end
  217. # 获取内容预览
  218. def get_content_preview
  219. return nil unless report&.target_content&.respond_to?(:content)
  220. content = report.target_content.content
  221. content ? content.truncate(100) : ''
  222. end
  223. # 判断是否应该发送邮件
  224. def should_send_email?(user, type)
  225. return false unless user.respond_to?(:email_notifications)
  226. return false unless user.email_notifications?
  227. return false unless user.respond_to?(:moderation_email_notifications)
  228. case type
  229. when :new_report
  230. user.moderation_email_notifications?
  231. when :daily_summary
  232. user.daily_summary_emails?
  233. else
  234. true
  235. end
  236. end
  237. # 判断是否应该向用户发送邮件
  238. def should_send_email_to_user?(user, type)
  239. return false unless user.respond_to?(:email_notifications)
  240. return false unless user.email_notifications?
  241. case type
  242. when :report_status_change
  243. user.report_status_email_notifications?
  244. else
  245. true
  246. end
  247. end
  248. # 判断是否应该发送推送通知
  249. def should_send_push?(user, type)
  250. return false unless user.respond_to?(:push_notifications)
  251. return false unless user.push_notifications?
  252. case type
  253. when :new_report
  254. true # 高优先级通知
  255. when :daily_summary
  256. false # 摘要通知不需要推送
  257. else
  258. user.moderation_push_notifications?
  259. end
  260. end
  261. # 记录通知活动日志
  262. def log_notification_activity
  263. case notification_type
  264. when :new_report
  265. Rails.logger.info "Admins notified of new content report: Report##{report.id}"
  266. when :status_change
  267. Rails.logger.info "Reporter notified of status change: Report##{report.id} -> #{report.status}"
  268. when :content_action
  269. Rails.logger.info "Content author notified of moderation action: Report##{report.id}"
  270. when :daily_summary
  271. Rails.logger.info "Daily moderation summary sent for #{options[:date]}"
  272. end
  273. end
  274. end

app/services/notification_service.rb

0.0% lines covered

196 relevant lines. 0 lines covered and 196 lines missed.
    
  1. # 通知服务
  2. # 负责管理系统中各种用户通知的创建、发送和管理
  3. class NotificationService < ApplicationService
  4. include ServiceInterface
  5. attr_reader :recipient, :actor, :notifiable, :notification_type, :title, :content
  6. def initialize(recipient:, actor:, notifiable:, notification_type:, title:, content:)
  7. super()
  8. @recipient = recipient
  9. @actor = actor
  10. @notifiable = notifiable
  11. @notification_type = notification_type
  12. @title = title
  13. @content = content
  14. end
  15. def call
  16. handle_errors do
  17. validate_parameters
  18. create_notification
  19. log_notification_created
  20. self
  21. end
  22. end
  23. # 类方法:小红花相关通知
  24. class << self
  25. # 发送小红花通知
  26. def send_flower_notification(recipient, actor, flower)
  27. return false if should_skip_notification?(recipient, actor, Notification::NOTIFICATION_TYPES[:flower_received])
  28. notification = Notification.create_flower_notification(recipient, actor, flower)
  29. log_notification("小红花通知", notification)
  30. notification
  31. end
  32. # 发送评论通知
  33. def send_comment_notification(recipient, actor, comment)
  34. return false if should_skip_notification?(recipient, actor, Notification::NOTIFICATION_TYPES[:flower_comment])
  35. notification = Notification.create_comment_notification(recipient, actor, comment)
  36. log_notification("评论通知", notification)
  37. notification
  38. end
  39. # 发送活动更新通知
  40. def send_activity_update_notification(recipient, actor, event, update_type, message)
  41. return false if should_skip_notification?(recipient, actor, Notification::NOTIFICATION_TYPES[:activity_update])
  42. notification = Notification.create_activity_notification(recipient, actor, event, update_type, message)
  43. log_notification("活动更新通知", notification)
  44. notification
  45. end
  46. # 发送活动审批通知
  47. def send_event_approval_notification(recipient, actor, event, approved)
  48. notification_type = approved ? Notification::NOTIFICATION_TYPES[:event_approved] : Notification::NOTIFICATION_TYPES[:event_rejected]
  49. return false if should_skip_notification?(recipient, actor, notification_type)
  50. notification = Notification.create_event_approval_notification(recipient, actor, event, approved)
  51. log_notification("活动审批通知", notification)
  52. notification
  53. end
  54. # 批量发送通知
  55. def send_bulk_notifications(recipients, actor, notifiable, notification_type, title, content)
  56. return [] if recipients.blank?
  57. notifications = []
  58. recipients.each do |recipient|
  59. next if should_skip_notification?(recipient, actor, notification_type)
  60. notification = Notification.create!(
  61. recipient: recipient,
  62. actor: actor,
  63. notifiable: notifiable,
  64. notification_type: notification_type,
  65. title: title,
  66. content: content
  67. )
  68. notifications << notification
  69. end
  70. log_bulk_notification(notification_type, notifications.count)
  71. notifications
  72. end
  73. # 发送系统通知
  74. def send_system_notification(recipients, title, content, options = {})
  75. actor = options[:actor] || User.find_by(role: 2) # root admin or default actor
  76. notifiable = options[:notifiable]
  77. notifications = []
  78. Array(recipients).each do |recipient|
  79. notification = Notification.create!(
  80. recipient: recipient,
  81. actor: actor,
  82. notifiable: notifiable || recipient, # 如果没有指定notifiable,使用recipient作为默认值
  83. notification_type: 'activity_update',
  84. title: title,
  85. content: content
  86. )
  87. notifications << notification
  88. end
  89. log_notification("系统通知", notifications)
  90. notifications
  91. end
  92. # 获取用户未读通知数量
  93. def unread_count_for(user)
  94. Notification.unread_count_for(user)
  95. end
  96. # 获取用户最近的通知
  97. def recent_notifications_for(user, limit = 10, include_read: false)
  98. scope = Notification.for_recipient(user).recent.limit(limit)
  99. scope = scope.unread unless include_read
  100. scope
  101. end
  102. # 标记通知为已读
  103. def mark_as_read(notification_id, user)
  104. notification = Notification.find_by(id: notification_id, recipient: user)
  105. return false unless notification
  106. notification.mark_as_read!
  107. true
  108. end
  109. # 批量标记为已读
  110. def mark_all_as_read_for(user)
  111. Notification.mark_all_as_read_for(user)
  112. log_notification_action("批量标记已读", user: user.id)
  113. end
  114. # 删除通知
  115. def delete_notification(notification_id, user)
  116. notification = Notification.find_by(id: notification_id, recipient: user)
  117. return false unless notification
  118. notification.destroy
  119. log_notification_action("删除通知", user: user.id, notification: notification_id)
  120. true
  121. end
  122. # 批量删除通知
  123. def delete_notifications(notification_ids, user)
  124. notifications = Notification.where(id: notification_ids, recipient: user)
  125. deleted_count = notifications.count
  126. notifications.destroy_all
  127. log_notification_action("批量删除通知", user: user.id, count: deleted_count)
  128. deleted_count
  129. end
  130. # 清理过期通知
  131. def cleanup_old_notifications(days = 30)
  132. deleted_count = Notification.cleanup_old_notifications(days)
  133. log_notification_action("清理过期通知", days: days, count: deleted_count)
  134. deleted_count
  135. end
  136. # 获取通知统计
  137. def notification_stats_for(user, days = 7)
  138. notifications = Notification.for_recipient(user)
  139. .where('created_at >= ?', days.days.ago)
  140. {
  141. total_count: notifications.count,
  142. unread_count: notifications.unread.count,
  143. by_type: notifications.group(:notification_type).count,
  144. recent_count: notifications.where('created_at >= ?', 1.day.ago).count
  145. }
  146. end
  147. # 检查用户是否有新通知
  148. def has_new_notifications?(user, since: nil)
  149. scope = Notification.for_recipient(user).unread
  150. scope = scope.where('created_at > ?', since) if since
  151. scope.exists?
  152. end
  153. # 获取用户的通知偏好设置(预留接口)
  154. def notification_preferences(user)
  155. # TODO: 实现用户通知偏好设置
  156. {
  157. flower_received: true,
  158. flower_comment: true,
  159. activity_update: true,
  160. event_approved: true,
  161. event_rejected: true
  162. }
  163. end
  164. # 检查用户是否接收某种类型的通知
  165. def should_receive_notification?(user, notification_type)
  166. preferences = notification_preferences(user)
  167. preferences[notification_type.to_sym]
  168. end
  169. private
  170. # 判断是否应该跳过通知发送
  171. def should_skip_notification?(recipient, actor, notification_type = nil)
  172. return true if recipient.nil? || actor.nil?
  173. return true if recipient.id == actor.id # 不给自己发通知
  174. return true unless notification_type && should_receive_notification?(recipient, notification_type)
  175. false
  176. end
  177. # 记录通知日志
  178. def log_notification(type, notification)
  179. if notification.is_a?(Array)
  180. Rails.logger.info "批量通知创建成功: #{type} - 数量: #{notification.count}"
  181. else
  182. Rails.logger.info "通知创建成功: #{type} - 接收者: #{notification.recipient&.id}, 类型: #{notification.notification_type}"
  183. end
  184. end
  185. # 记录批量通知日志
  186. def log_bulk_notification(type, count)
  187. Rails.logger.info "批量通知创建成功: #{type} - 数量: #{count}"
  188. end
  189. # 记录通知操作日志
  190. def log_notification_action(action, **params)
  191. Rails.logger.info "通知操作: #{action} - #{params}"
  192. end
  193. end
  194. private
  195. # 验证参数
  196. def validate_parameters
  197. errors.add(:recipient, "接收者不能为空") if recipient.blank?
  198. errors.add(:actor, "发送者不能为空") if actor.blank?
  199. errors.add(:notification_type, "通知类型不能为空") if notification_type.blank?
  200. errors.add(:title, "标题不能为空") if title.blank?
  201. errors.add(:content, "内容不能为空") if content.blank?
  202. # 验证通知类型
  203. unless Notification::NOTIFICATION_TYPES.values.include?(notification_type)
  204. errors.add(:notification_type, "无效的通知类型")
  205. end
  206. # 验证接收者和发送者不是同一人
  207. if recipient.present? && actor.present? && recipient.id == actor.id
  208. errors.add(:base, "不能给自己发送通知")
  209. end
  210. end
  211. # 创建通知
  212. def create_notification
  213. @notification = Notification.create!(
  214. recipient: recipient,
  215. actor: actor,
  216. notifiable: notifiable,
  217. notification_type: notification_type,
  218. title: title,
  219. content: content
  220. )
  221. end
  222. # 记录通知创建日志
  223. def log_notification_created
  224. Rails.logger.info "通知创建成功: 类型: #{notification_type}, 接收者: #{recipient.id}, 发送者: #{actor.id}"
  225. end
  226. end

app/services/optimized_pagination_service.rb

0.0% lines covered

161 relevant lines. 0 lines covered and 161 lines missed.
    
  1. # frozen_string_literal: true
  2. # OptimizedPaginationService - 高性能分页服务
  3. # 使用cursor-based pagination避免OFFSET性能问题
  4. class OptimizedPaginationService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :relation, :page, :per_page, :cursor, :order_field, :order_direction
  7. def initialize(relation:, page: nil, per_page: 20, cursor: nil, order_field: :created_at, order_direction: :desc)
  8. super()
  9. @relation = relation
  10. @page = page
  11. @per_page = per_page
  12. @cursor = cursor
  13. @order_field = order_field
  14. @order_direction = order_direction
  15. end
  16. def call
  17. handle_errors do
  18. validate_parameters
  19. paginate
  20. end
  21. self
  22. end
  23. # 类方法:快速分页
  24. def self.paginate(relation, page: 1, per_page: 20, cursor: nil, order_field: :created_at, order_direction: :desc)
  25. new(
  26. relation: relation,
  27. page: page,
  28. per_page: per_page,
  29. cursor: cursor,
  30. order_field: order_field,
  31. order_direction: order_direction
  32. ).call
  33. end
  34. # 类方法:cursor-based分页(适用于无限滚动)
  35. def self.cursor_paginate(relation, cursor: nil, per_page: 20, order_field: :created_at, order_direction: :desc)
  36. new(
  37. relation: relation,
  38. cursor: cursor,
  39. per_page: per_page,
  40. order_field: order_field,
  41. order_direction: order_direction
  42. ).call
  43. end
  44. def data
  45. @data ||= {}
  46. end
  47. def has_next_page?
  48. data[:has_next_page]
  49. end
  50. def has_prev_page?
  51. data[:has_prev_page]
  52. end
  53. def next_cursor
  54. data[:next_cursor]
  55. end
  56. def prev_cursor
  57. data[:prev_cursor]
  58. end
  59. def total_count
  60. data[:total_count]
  61. end
  62. private
  63. def validate_parameters
  64. errors.add(:relation, "查询对象不能为空") if relation.blank?
  65. errors.add(:per_page, "每页数量必须大于0") if per_page.to_i <= 0
  66. errors.add(:per_page, "每页数量不能超过100") if per_page.to_i > 100
  67. if cursor && page
  68. errors.add(:base, "不能同时使用cursor和page分页")
  69. end
  70. # 验证排序字段是否存在
  71. if order_field.present? && !relation.column_names.include?(order_field.to_s)
  72. errors.add(:order_field, "无效的排序字段")
  73. end
  74. end
  75. def paginate
  76. if cursor.present?
  77. cursor_based_pagination
  78. else
  79. offset_based_pagination
  80. end
  81. end
  82. # 基于OFFSET的传统分页
  83. def offset_based_pagination
  84. page_num = [page.to_i, 1].max
  85. offset_value = (page_num - 1) * per_page
  86. # 获取总记录数(可选,用于显示分页信息)
  87. if should_count_total?
  88. total_records = relation.count
  89. else
  90. total_records = nil
  91. end
  92. # 执行分页查询
  93. paginated_relation = relation
  94. .limit(per_page + 1) # 多查询一条用于判断是否有下一页
  95. .offset(offset_value)
  96. .order(order_direction_sql)
  97. records = paginated_relation.to_a
  98. has_next = records.length > per_page
  99. records.pop if has_next # 移除多查询的记录
  100. data.merge!({
  101. records: records,
  102. current_page: page_num,
  103. per_page: per_page,
  104. has_next_page: has_next,
  105. has_prev_page: page_num > 1,
  106. total_count: total_records,
  107. total_pages: total_records ? (total_records.to_f / per_page).ceil : nil
  108. })
  109. self
  110. end
  111. # 基于cursor的高性能分页
  112. def cursor_based_pagination
  113. # 解析cursor
  114. cursor_value = decode_cursor(cursor) if cursor
  115. # 构建查询条件
  116. query_relation = relation
  117. if cursor_value
  118. query_relation = query_relation.where(cursor_condition(cursor_value))
  119. end
  120. # 执行查询,多查询一条用于判断是否有下一页
  121. paginated_relation = query_relation
  122. .limit(per_page + 1)
  123. .order(order_direction_sql)
  124. records = paginated_relation.to_a
  125. has_next = records.length > per_page
  126. records.pop if has_next
  127. # 生成cursor信息
  128. next_cursor_value = records.last ? records.last[order_field] : nil
  129. prev_cursor_value = records.first ? records.first[order_field] : nil
  130. data.merge!({
  131. records: records,
  132. per_page: per_page,
  133. has_next_page: has_next,
  134. has_prev_page: cursor.present?,
  135. next_cursor: next_cursor_value ? encode_cursor(next_cursor_value) : nil,
  136. prev_cursor: prev_cursor_value ? encode_cursor(prev_cursor_value) : nil
  137. })
  138. self
  139. end
  140. def order_direction_sql
  141. case order_direction.to_sym
  142. when :asc
  143. "#{order_field} ASC"
  144. when :desc
  145. "#{order_field} DESC"
  146. else
  147. "#{order_field} DESC" # 默认降序
  148. end
  149. end
  150. def cursor_condition(cursor_value)
  151. case order_direction.to_sym
  152. when :asc
  153. "#{order_field} > ?"
  154. when :desc
  155. "#{order_field} < ?"
  156. else
  157. "#{order_field} < ?" # 默认降序
  158. end
  159. end
  160. def encode_cursor(value)
  161. # Base64编码cursor值
  162. Base64.urlsafe_encode64("#{value}:#{Time.current.to_i}")
  163. end
  164. def decode_cursor(encoded_cursor)
  165. return nil unless encoded_cursor
  166. begin
  167. decoded = Base64.urlsafe_decode64(encoded_cursor)
  168. decoded.split(':').first
  169. rescue
  170. nil
  171. end
  172. end
  173. def should_count_total?
  174. # 只有在第一页时才计算总数,避免性能问题
  175. page.to_i <= 1 && !cursor
  176. end
  177. end

app/services/pagination_service.rb

0.0% lines covered

211 relevant lines. 0 lines covered and 211 lines missed.
    
  1. # frozen_string_literal: true
  2. # 分页服务
  3. # 提供多种分页策略,优化大数据集的查询性能
  4. class PaginationService
  5. class << self
  6. # 基于偏移量的传统分页
  7. # @param scope [ActiveRecord::Relation] 查询范围
  8. # @param page [Integer] 页码(从1开始)
  9. # @param per_page [Integer] 每页记录数
  10. # @param options [Hash] 额外选项
  11. # @return [Hash] 分页结果
  12. def paginate_by_offset(scope, page: 1, per_page: 20, options = {})
  13. page = [page.to_i, 1].max
  14. per_page = [[per_page.to_i, 1].max, 100].min # 限制最大每页100条
  15. total_count = QueryOptimizationService.optimized_count_query(
  16. scope,
  17. options[:cache_key] ? "count_#{options[:cache_key]}" : nil,
  18. options[:cache_ttl] || 5.minutes
  19. )
  20. total_pages = (total_count.to_f / per_page).ceil
  21. offset = (page - 1) * per_page
  22. records = scope.limit(per_page).offset(offset)
  23. records = QueryOptimizationService.preload_associations(records, options[:includes]) if options[:includes]
  24. {
  25. records: records,
  26. pagination: {
  27. current_page: page,
  28. per_page: per_page,
  29. total_count: total_count,
  30. total_pages: total_pages,
  31. has_next_page: page < total_pages,
  32. has_prev_page: page > 1,
  33. next_page: page < total_pages ? page + 1 : nil,
  34. prev_page: page > 1 ? page - 1 : nil
  35. }
  36. }
  37. end
  38. # 基于游标的分页(性能更好,适合大数据集)
  39. # @param scope [ActiveRecord::Relation] 查询范围
  40. # @param cursor [String] 游标位置
  41. # @param limit [Integer] 每页记录数
  42. # @param options [Hash] 额外选项
  43. # @return [Hash] 分页结果
  44. def paginate_by_cursor(scope, cursor: nil, limit: 20, options = {})
  45. limit = [[limit.to_i, 1].max, 100].min
  46. order_column = options[:order_column] || 'created_at'
  47. order_direction = options[:order_direction] || 'desc'
  48. # 构建查询
  49. query = scope.limit(limit + 1) # 多查询一条来判断是否还有下一页
  50. # 添加游标条件
  51. if cursor
  52. operator = order_direction == 'desc' ? '<' : '>'
  53. query = query.where("#{order_column} #{operator} ?", decode_cursor(cursor))
  54. end
  55. # 排序
  56. query = query.order("#{order_column} #{order_direction}")
  57. # 预加载关联
  58. if options[:includes]
  59. records = QueryOptimizationService.preload_associations(query, options[:includes])
  60. else
  61. records = query.to_a
  62. end
  63. # 判断是否还有下一页
  64. has_next = records.length > limit
  65. records = records.first(limit) if has_next
  66. # 生成下一页游标
  67. next_cursor = nil
  68. if has_next && records.any?
  69. last_record = records.last
  70. next_cursor = encode_cursor(last_record.send(order_column))
  71. end
  72. {
  73. records: records,
  74. pagination: {
  75. next_cursor: next_cursor,
  76. has_next_page: has_next,
  77. limit: limit,
  78. order_column: order_column,
  79. order_direction: order_direction
  80. }
  81. }
  82. end
  83. # 搜索分页(结合搜索和分页)
  84. # @param scope [ActiveRecord::Relation] 查询范围
  85. # @param search_term [String] 搜索关键词
  86. # @param search_fields [Array] 搜索字段
  87. # @param pagination_options [Hash] 分页选项
  88. # @return [Hash] 搜索分页结果
  89. def search_and_paginate(scope, search_term: nil, search_fields: [], pagination_options: {})
  90. # 应用搜索条件
  91. if search_term.present? && search_fields.any?
  92. search_conditions = search_fields.map do |field|
  93. "#{field} ILIKE ?"
  94. end.join(' OR ')
  95. search_values = search_fields.map { search_term }
  96. scope = scope.where(search_conditions, *search_values)
  97. end
  98. # 执行分页
  99. if pagination_options[:cursor]
  100. paginate_by_cursor(scope, pagination_options)
  101. else
  102. paginate_by_offset(scope, pagination_options)
  103. end
  104. end
  105. # 无限滚动分页(适合移动端)
  106. # @param scope [ActiveRecord::Relation] 查询范围
  107. # @param last_id [Integer] 上一页最后一条记录的ID
  108. # @param limit [Integer] 加载记录数
  109. # @param options [Hash] 额外选项
  110. # @return [Hash] 分页结果
  111. def infinite_scroll(scope, last_id: nil, limit: 20, options = {})
  112. limit = [[limit.to_i, 1].max, 50].min # 无限滚动通常限制更多
  113. query = scope.limit(limit + 1)
  114. # 添加ID条件
  115. if last_id
  116. if options[:order_direction] == 'asc'
  117. query = query.where('id > ?', last_id)
  118. else
  119. query = query.where('id < ?', last_id)
  120. end
  121. end
  122. # 排序
  123. order_direction = options[:order_direction] || 'desc'
  124. query = query.order("id #{order_direction}")
  125. # 预加载关联
  126. if options[:includes]
  127. records = QueryOptimizationService.preload_associations(query, options[:includes])
  128. else
  129. records = query.to_a
  130. end
  131. # 判断是否还有更多数据
  132. has_more = records.length > limit
  133. records = records.first(limit) if has_more
  134. # 下一页的最后ID
  135. next_last_id = nil
  136. if has_more && records.any?
  137. next_last_id = records.last.id
  138. end
  139. {
  140. records: records,
  141. pagination: {
  142. next_last_id: next_last_id,
  143. has_more: has_more,
  144. limit: limit
  145. }
  146. }
  147. end
  148. # 时间范围分页
  149. # @param scope [ActiveRecord::Relation] 查询范围
  150. # @param time_field [String] 时间字段名
  151. # @param start_time [DateTime] 开始时间
  152. # @param end_time [DateTime] 结束时间
  153. # @param pagination_options [Hash] 分页选项
  154. # @return [Hash] 时间范围分页结果
  155. def paginate_by_time_range(scope, time_field: 'created_at', start_time: nil, end_time: nil, pagination_options: {})
  156. query = scope
  157. # 应用时间范围过滤
  158. if start_time
  159. query = query.where("#{time_field} >= ?", start_time)
  160. end
  161. if end_time
  162. query = query.where("#{time_field} <= ?", end_time)
  163. end
  164. # 按时间字段排序
  165. query = query.order("#{time_field} DESC")
  166. # 执行分页
  167. if pagination_options[:cursor]
  168. # 基于时间的游标分页
  169. cursor_based_time_pagination(query, time_field, pagination_options)
  170. else
  171. # 传统分页
  172. paginate_by_offset(query, pagination_options)
  173. end
  174. end
  175. # 分组分页(按某个字段分组后分页)
  176. # @param scope [ActiveRecord::Relation] 查询范围
  177. # @param group_field [String] 分组字段
  178. # @param pagination_options [Hash] 分页选项
  179. # @return [Hash] 分组分页结果
  180. def paginate_by_group(scope, group_field, pagination_options = {})
  181. per_page = pagination_options[:per_page] || 10
  182. page = pagination_options[:page] || 1
  183. # 获取分组数据
  184. grouped_data = scope.group(group_field)
  185. .select("#{group_field}, COUNT(*) as count")
  186. .order("COUNT(*) DESC")
  187. .to_a
  188. # 分页处理分组
  189. total_groups = grouped_data.length
  190. total_pages = (total_groups.to_f / per_page).ceil
  191. offset = (page - 1) * per_page
  192. paginated_groups = grouped_data[offset, per_page] || []
  193. # 获取每个分组的详细记录
  194. records = []
  195. paginated_groups.each do |group|
  196. group_records = scope.where(group_field => group.send(group_field))
  197. .limit(pagination_options[:per_group_limit] || 5)
  198. records.concat(group_records)
  199. end
  200. {
  201. records: records,
  202. groups: paginated_groups,
  203. pagination: {
  204. current_page: page,
  205. per_page: per_page,
  206. total_groups: total_groups,
  207. total_pages: total_pages,
  208. has_next_page: page < total_pages,
  209. has_prev_page: page > 1
  210. }
  211. }
  212. end
  213. # 元数据分页(只返回分页信息,不返回具体记录)
  214. # @param scope [ActiveRecord::Relation] 查询范围
  215. # @param per_page [Integer] 每页记录数
  216. # @param options [Hash] 额外选项
  217. # @return [Hash] 分页元数据
  218. def pagination_metadata(scope, per_page: 20, options = {})
  219. total_count = QueryOptimizationService.optimized_count_query(
  220. scope,
  221. options[:cache_key] ? "metadata_#{options[:cache_key]}" : nil,
  222. options[:cache_ttl] || 10.minutes
  223. )
  224. total_pages = (total_count.to_f / per_page).ceil
  225. {
  226. total_count: total_count,
  227. total_pages: total_pages,
  228. per_page: per_page,
  229. first_page: 1,
  230. last_page: total_pages,
  231. page_range: calculate_page_range(total_pages, options[:current_page] || 1)
  232. }
  233. end
  234. private
  235. # 编码游标
  236. def encode_cursor(value)
  237. Base64.urlsafe_encode64(value.to_s)
  238. end
  239. # 解码游标
  240. def decode_cursor(cursor)
  241. Base64.urlsafe_decode64(cursor)
  242. rescue
  243. nil
  244. end
  245. # 基于时间的游标分页
  246. def cursor_based_time_pagination(scope, time_field, options)
  247. cursor = options[:cursor]
  248. limit = options[:limit] || 20
  249. query = scope.limit(limit + 1)
  250. if cursor
  251. decoded_time = decode_cursor(cursor)
  252. query = query.where("#{time_field} < ?", decoded_time) if decoded_time
  253. end
  254. query = query.order("#{time_field} DESC")
  255. records = query.to_a
  256. has_next = records.length > limit
  257. records = records.first(limit) if has_next
  258. next_cursor = nil
  259. if has_next && records.any?
  260. next_cursor = encode_cursor(records.last.send(time_field).iso8601)
  261. end
  262. {
  263. records: records,
  264. pagination: {
  265. next_cursor: next_cursor,
  266. has_next_page: has_next,
  267. limit: limit
  268. }
  269. }
  270. end
  271. # 计算页码范围(用于显示页码导航)
  272. def calculate_page_range(total_pages, current_page, window_size: 5)
  273. return [] if total_pages == 0
  274. start_page = [current_page - window_size / 2, 1].max
  275. end_page = [start_page + window_size - 1, total_pages].min
  276. start_page = [end_page - window_size + 1, 1].max if end_page - start_page + 1 < window_size
  277. (start_page..end_page).to_a
  278. end
  279. end
  280. end

app/services/permission_check_service.rb

0.0% lines covered

148 relevant lines. 0 lines covered and 148 lines missed.
    
  1. # frozen_string_literal: true
  2. # PermissionCheckService - 统一权限验证服务
  3. # 整合各种权限检查逻辑,提供统一的权限验证接口
  4. class PermissionCheckService < ApplicationService
  5. attr_reader :user, :resource, :action, :context
  6. def initialize(user:, resource:, action:, context: {})
  7. super()
  8. @user = user
  9. @resource = resource
  10. @action = action
  11. @context = context
  12. end
  13. # 主要调用方法
  14. def call
  15. handle_errors do
  16. return failure!("用户不能为空") unless user
  17. return failure!("资源不能为空") unless resource
  18. case action
  19. when :approve_events
  20. check_approve_events_permission
  21. when :manage_users
  22. check_manage_users_permission
  23. when :view_admin_panel
  24. check_view_admin_panel_permission
  25. when :manage_system
  26. check_manage_system_permission
  27. when :edit_post
  28. check_edit_post_permission
  29. when :hide_post
  30. check_hide_post_permission
  31. when :pin_post
  32. check_pin_post_permission
  33. when :manage_event
  34. check_manage_event_permission
  35. when :claim_leadership
  36. check_claim_leadership_permission
  37. when :complete_event
  38. check_complete_event_permission
  39. else
  40. failure!("不支持的权限检查: #{action}")
  41. end
  42. end
  43. end
  44. # 类方法:快速权限检查
  45. def self.can?(user, resource, action, context = {})
  46. new(user: user, resource: resource, action: action, context: context).call.success?
  47. end
  48. private
  49. # 检查活动审批权限
  50. def check_approve_events_permission
  51. if user.can_approve_events?
  52. success!
  53. else
  54. failure!("用户 #{user.nickname} 没有审批活动的权限")
  55. end
  56. end
  57. # 检查用户管理权限
  58. def check_manage_users_permission
  59. if user.can_manage_users?
  60. success!
  61. else
  62. failure!("用户 #{user.nickname} 没有管理用户的权限")
  63. end
  64. end
  65. # 检查管理面板查看权限
  66. def check_view_admin_panel_permission
  67. if user.can_view_admin_panel?
  68. success!
  69. else
  70. failure!("用户 #{user.nickname} 没有查看管理面板的权限")
  71. end
  72. end
  73. # 检查系统管理权限
  74. def check_manage_system_permission
  75. if user.can_manage_system?
  76. success!
  77. else
  78. failure!("用户 #{user.nickname} 没有系统管理权限")
  79. end
  80. end
  81. # 检查帖子编辑权限
  82. def check_edit_post_permission
  83. if resource.is_a?(Post)
  84. if resource.can_edit?(user)
  85. success!
  86. else
  87. failure!("用户 #{user.nickname} 没有编辑此帖子的权限")
  88. end
  89. else
  90. failure!("资源类型不正确,期望Post")
  91. end
  92. end
  93. # 检查帖子隐藏权限
  94. def check_hide_post_permission
  95. if resource.is_a?(Post)
  96. if resource.can_hide?(user)
  97. success!
  98. else
  99. failure!("用户 #{user.nickname} 没有隐藏此帖子的权限")
  100. end
  101. else
  102. failure!("资源类型不正确,期望Post")
  103. end
  104. end
  105. # 检查帖子置顶权限
  106. def check_pin_post_permission
  107. if resource.is_a?(Post)
  108. if resource.can_pin?(user)
  109. success!
  110. else
  111. failure!("用户 #{user.nickname} 没有置顶此帖子的权限")
  112. end
  113. else
  114. failure!("资源类型不正确,期望Post")
  115. end
  116. end
  117. # 检查活动管理权限
  118. def check_manage_event_permission
  119. if resource.is_a?(ReadingEvent)
  120. # 活动创建者或管理员可以管理活动
  121. if resource.leader_id == user.id || user.any_admin?
  122. success!
  123. else
  124. failure!("用户 #{user.nickname} 没有管理此活动的权限")
  125. end
  126. else
  127. failure!("资源类型不正确,期望ReadingEvent")
  128. end
  129. end
  130. # 检查领读报名权限
  131. def check_claim_leadership_permission
  132. if resource.is_a?(ReadingEvent) && context[:schedule]
  133. schedule = context[:schedule]
  134. # 检查是否是自由报名模式
  135. unless resource.leader_assignment_type == 'voluntary'
  136. return failure!("该活动不支持自由报名领读")
  137. end
  138. # 检查是否已报名该活动
  139. unless user.enrollments.exists?(reading_event: resource)
  140. return failure!("请先报名该活动")
  141. end
  142. # 检查是否已有领读人
  143. if schedule.daily_leader.present?
  144. return failure!("该日已有领读人")
  145. end
  146. # 检查领读次数限制
  147. leadership_count = resource.reading_schedules.where(daily_leader: user).count
  148. if leadership_count >= 3
  149. return failure!("领读次数已达上限")
  150. end
  151. success!
  152. else
  153. failure!("资源类型或上下文不正确,期望ReadingEvent和schedule")
  154. end
  155. end
  156. # 检查活动完成权限
  157. def check_complete_event_permission
  158. if resource.is_a?(ReadingEvent)
  159. # 只有活动小组长可以结束活动
  160. if resource.current_leader?(user)
  161. success!
  162. else
  163. failure!("只有活动小组长可以结束活动")
  164. end
  165. else
  166. failure!("资源类型不正确,期望ReadingEvent")
  167. end
  168. end
  169. end

app/services/post_creation_service.rb

0.0% lines covered

81 relevant lines. 0 lines covered and 81 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostCreationService - 帖子创建服务
  3. # 专门负责帖子的创建逻辑,包括内容验证、分类处理等
  4. class PostCreationService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :user, :post_params, :post
  7. def initialize(user:, post_params:)
  8. super()
  9. @user = user
  10. @post_params = post_params
  11. @post = nil
  12. end
  13. # 创建帖子
  14. def call
  15. handle_errors do
  16. validate_creation_params
  17. create_post
  18. process_post_creation
  19. format_success_response
  20. end
  21. self
  22. end
  23. private
  24. # 验证创建参数
  25. def validate_creation_params
  26. return failure!("用户不能为空") unless user
  27. return failure!("用户不存在") unless user.persisted?
  28. return failure!("标题不能为空") if post_params[:title].blank?
  29. return failure!("内容不能为空") if post_params[:content].blank?
  30. # 验证内容长度
  31. if post_params[:content].length < 10
  32. return failure!("内容长度不能少于10个字符")
  33. end
  34. if post_params[:content].length > 10000
  35. return failure!("内容长度不能超过10000个字符")
  36. end
  37. # 验证标题长度
  38. if post_params[:title].length > 100
  39. return failure!("标题长度不能超过100个字符")
  40. end
  41. end
  42. # 创建帖子记录
  43. def create_post
  44. @post = user.posts.new(post_params)
  45. unless @post.save
  46. failure!(@post.errors.full_messages)
  47. return false
  48. end
  49. true
  50. end
  51. # 处理帖子创建后的逻辑
  52. def process_post_creation
  53. # 处理标签
  54. process_tags if @post.tags.present?
  55. # 处理图片
  56. process_images if @post.images.present?
  57. # 更新用户统计
  58. update_user_stats
  59. # 记录创建日志
  60. log_creation_event
  61. # 发送通知(如果需要)
  62. send_creation_notifications
  63. end
  64. # 处理标签
  65. def process_tags
  66. # 标签规范化处理
  67. tags = @post.tags.map(&:strip).reject(&:blank?).uniq
  68. @post.update!(tags: tags)
  69. end
  70. # 处理图片
  71. def process_images
  72. # 图片URL验证和处理
  73. valid_images = @post.images.select { |url| valid_image_url?(url) }
  74. @post.update!(images: valid_images) if valid_images.size != @post.images.size
  75. end
  76. # 验证图片URL
  77. def valid_image_url?(url)
  78. uri = URI.parse(url)
  79. uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
  80. rescue URI::InvalidURIError
  81. false
  82. end
  83. # 更新用户统计
  84. def update_user_stats
  85. # 检查用户模型是否有posts_count字段
  86. user.increment!(:posts_count) if user.respond_to?(:posts_count)
  87. end
  88. # 记录创建事件
  89. def log_creation_event
  90. Rails.logger.info "Post created: ID #{@post.id} by User #{user.id}"
  91. end
  92. # 发送创建通知
  93. def send_creation_notifications
  94. # 这里可以添加通知逻辑,比如通知关注者
  95. # NotificationService.post_created_notification(@post)
  96. end
  97. # 格式化成功响应
  98. def format_success_response
  99. success!({
  100. message: "帖子创建成功",
  101. post: post_data(@post)
  102. })
  103. end
  104. # 格式化帖子数据
  105. def post_data(post)
  106. post.as_json_for_api(current_user: user, include_stats: true)
  107. end
  108. end

app/services/post_data_service.rb

0.0% lines covered

338 relevant lines. 0 lines covered and 338 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostDataService - 帖子数据服务
  3. # 专门负责帖子数据的格式化、序列化和展示逻辑
  4. class PostDataService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :post, :current_user, :options
  7. def initialize(post:, current_user: nil, options: {})
  8. super()
  9. @post = post
  10. @current_user = current_user
  11. @options = options.with_indifferent_access
  12. end
  13. # 格式化帖子数据
  14. def call
  15. handle_errors do
  16. validate_data_params
  17. format_post_data
  18. end
  19. self
  20. end
  21. # 生成帖子摘要
  22. def generate_summary(length: 100)
  23. return "" unless post&.content
  24. # 移除HTML标签(如果有)
  25. plain_content = post.content.gsub(/<[^>]+>/, '').strip
  26. # 截取指定长度
  27. if plain_content.length > length
  28. plain_content[0...length] + "..."
  29. else
  30. plain_content
  31. end
  32. end
  33. # 生成帖子统计信息
  34. def generate_stats
  35. return {} unless post
  36. {
  37. views_count: post.respond_to?(:views_count) ? post.views_count : 0,
  38. likes_count: post.respond_to?(:likes_count) ? post.likes_count : 0,
  39. comments_count: post.respond_to?(:comments_count) ? post.comments_count : 0,
  40. shares_count: post.respond_to?(:shares_count) ? post.shares_count : 0,
  41. bookmarks_count: post.respond_to?(:bookmarks_count) ? post.bookmarks_count : 0
  42. }
  43. end
  44. # 生成时间相关数据
  45. def generate_time_data
  46. return {} unless post
  47. {
  48. created_at: post.created_at,
  49. updated_at: post.updated_at,
  50. time_ago: time_ago_in_words(post.created_at),
  51. last_activity_ago: time_ago_in_words(post.updated_at)
  52. }
  53. end
  54. # 生成作者信息
  55. def generate_author_info
  56. return {} unless post&.user
  57. {
  58. id: post.user.id,
  59. nickname: post.user.nickname,
  60. avatar_url: post.user.avatar_url,
  61. role: post.user.role_display_name,
  62. is_verified: post.user.respond_to?(:verified?) ? post.user.verified? : false,
  63. followers_count: post.user.respond_to?(:followers_count) ? post.user.followers_count : 0,
  64. posts_count: post.user.respond_to?(:posts_count) ? post.user.posts_count : 0
  65. }
  66. end
  67. # 生成交互状态信息
  68. def generate_interaction_states
  69. return {} unless current_user && post
  70. {
  71. liked: post.liked_by?(current_user),
  72. bookmarked: post.bookmarked_by?(current_user),
  73. can_edit: PostPermissionService.can_edit?(post, current_user),
  74. can_delete: PostPermissionService.can_delete?(post, current_user),
  75. can_pin: PostPermissionService.can_pin?(post, current_user),
  76. can_hide: PostPermissionService.can_hide?(post, current_user),
  77. can_comment: PostPermissionService.can_comment?(post, current_user)
  78. }
  79. end
  80. # 生成分类信息
  81. def generate_category_info
  82. return {} unless post
  83. {
  84. category: post.category,
  85. category_name: post.category_name,
  86. category_color: category_color(post.category)
  87. }
  88. end
  89. # 生成标签信息
  90. def generate_tags_info
  91. return [] unless post&.tags
  92. post.tags.map do |tag|
  93. {
  94. name: tag,
  95. color: tag_color(tag),
  96. count: tag_post_count(tag)
  97. }
  98. end
  99. end
  100. # 生成图片信息
  101. def generate_images_info
  102. return [] unless post&.images
  103. post.images.map.with_index do |image_url, index|
  104. {
  105. url: image_url,
  106. thumbnail: thumbnail_url(image_url),
  107. alt: "#{post.title} - 图片#{index + 1}",
  108. width: image_width(image_url),
  109. height: image_height(image_url)
  110. }
  111. end
  112. end
  113. private
  114. # 验证数据参数
  115. def validate_data_params
  116. return failure!("帖子不能为空") unless post
  117. return failure!("帖子不存在") unless post.persisted?
  118. true
  119. end
  120. # 格式化帖子数据
  121. def format_post_data
  122. data = {
  123. id: post.id,
  124. title: post.title,
  125. content: formatted_content,
  126. summary: generate_summary,
  127. stats: generate_stats,
  128. time_data: generate_time_data,
  129. author: generate_author_info,
  130. category: generate_category_info,
  131. tags: generate_tags_info,
  132. images: generate_images_info,
  133. interactions: generate_interaction_states,
  134. metadata: generate_metadata
  135. }
  136. # 根据选项添加额外字段
  137. data.merge!(add_optional_fields)
  138. success!(data)
  139. end
  140. # 格式化内容
  141. def formatted_content
  142. return post.content unless options[:format_content]
  143. case options[:content_format]
  144. when :html
  145. format_content_as_html
  146. when :markdown
  147. format_content_as_markdown
  148. when :plain
  149. format_content_as_plain
  150. else
  151. post.content
  152. end
  153. end
  154. # HTML格式化
  155. def format_content_as_html
  156. # 这里可以添加HTML格式化逻辑
  157. post.content
  158. end
  159. # Markdown格式化
  160. def format_content_as_markdown
  161. # 这里可以添加Markdown格式化逻辑
  162. post.content
  163. end
  164. # 纯文本格式化
  165. def format_content_as_plain
  166. post.content.gsub(/<[^>]+>/, '').strip
  167. end
  168. # 生成元数据
  169. def generate_metadata
  170. {
  171. pinned: post.pinned?,
  172. hidden: post.hidden?,
  173. deleted: post.deleted?,
  174. featured: post.featured?,
  175. priority: post.priority || 0,
  176. source: post.source || 'web',
  177. device_type: post.device_type || 'unknown'
  178. }
  179. end
  180. # 添加可选字段
  181. def add_optional_fields
  182. additional_fields = {}
  183. # 包含完整内容
  184. if options[:include_full_content]
  185. additional_fields[:full_content] = post.content
  186. end
  187. # 包含SEO信息
  188. if options[:include_seo]
  189. additional_fields[:seo] = generate_seo_data
  190. end
  191. # 包含分享信息
  192. if options[:include_share]
  193. additional_fields[:share] = generate_share_data
  194. end
  195. # 包含相关帖子
  196. if options[:include_related]
  197. additional_fields[:related_posts] = generate_related_posts
  198. end
  199. additional_fields
  200. end
  201. # 生成SEO数据
  202. def generate_seo_data
  203. {
  204. title: post.title,
  205. description: generate_summary(length: 160),
  206. keywords: post.tags&.join(', '),
  207. url: post_url(post),
  208. image_url: post.images&.first
  209. }
  210. end
  211. # 生成分享数据
  212. def generate_share_data
  213. {
  214. url: post_url(post),
  215. title: post.title,
  216. description: generate_summary,
  217. image_url: post.images&.first
  218. }
  219. end
  220. # 生成相关帖子
  221. def generate_related_posts
  222. # 这里可以添加相关帖子推荐逻辑
  223. []
  224. end
  225. # 辅助方法:时间格式化
  226. def time_ago_in_words(time)
  227. return "" unless time
  228. seconds = Time.current - time
  229. minutes = seconds / 60
  230. hours = minutes / 60
  231. days = hours / 24
  232. if days > 0
  233. "#{days.to_i}天前"
  234. elsif hours > 0
  235. "#{hours.to_i}小时前"
  236. elsif minutes > 0
  237. "#{minutes.to_i}分钟前"
  238. else
  239. "刚刚"
  240. end
  241. end
  242. # 辅助方法:分类颜色
  243. def category_color(category)
  244. colors = {
  245. 'reading' => '#FF6B6B',
  246. 'discussion' => '#4ECDC4',
  247. 'share' => '#45B7D1',
  248. 'question' => '#96CEB4',
  249. 'announcement' => '#FECA57'
  250. }
  251. colors[category] || '#95A5A6'
  252. end
  253. # 辅助方法:标签颜色
  254. def tag_color(tag)
  255. # 简单的标签颜色生成算法
  256. hash = Digest::MD5.hexdigest(tag)[0..5]
  257. "##{hash}"
  258. end
  259. # 辅助方法:标签帖子数量
  260. def tag_post_count(tag)
  261. # 这里可以添加缓存逻辑
  262. Post.where('tags LIKE ?', "%#{tag}%").count
  263. end
  264. # 辅助方法:缩略图URL
  265. def thumbnail_url(image_url)
  266. # 这里可以添加缩略图生成逻辑
  267. image_url
  268. end
  269. # 辅助方法:图片宽度
  270. def image_width(image_url)
  271. # 这里可以添加图片尺寸获取逻辑
  272. 800
  273. end
  274. # 辅助方法:图片高度
  275. def image_height(image_url)
  276. # 这里可以添加图片尺寸获取逻辑
  277. 600
  278. end
  279. # 辅助方法:帖子URL
  280. def post_url(post)
  281. "/posts/#{post.id}"
  282. end
  283. private
  284. # 使用预获取权限的帖子格式化方法
  285. def format_post_with_permissions(post, current_user: nil, options: {}, permissions: {})
  286. data = {
  287. id: post.id,
  288. title: post.title,
  289. content: formatted_content,
  290. summary: generate_summary,
  291. stats: generate_stats,
  292. time_data: generate_time_data,
  293. author: generate_author_info,
  294. category: generate_category_info,
  295. tags: generate_tags_info,
  296. images: generate_images_info,
  297. interactions: generate_interaction_states_with_permissions(post, current_user, permissions),
  298. metadata: generate_metadata
  299. }
  300. # 根据选项添加额外字段
  301. data.merge!(add_optional_fields)
  302. data
  303. end
  304. # 生成带预获取权限的交互状态
  305. def generate_interaction_states_with_permissions(post, current_user, permissions)
  306. return {} unless current_user && post
  307. post_id = post.id
  308. # 使用预获取的权限信息,如果没有则回退到原有方法
  309. {
  310. liked: post.liked_by?(current_user),
  311. bookmarked: post.bookmarked_by?(current_user),
  312. can_edit: permissions.dig(:edit, post_id) || PostPermissionService.can_edit?(post, current_user),
  313. can_delete: permissions.dig(:delete, post_id) || PostPermissionService.can_delete?(post, current_user),
  314. can_pin: permissions.dig(:pin, post_id) || PostPermissionService.can_pin?(post, current_user),
  315. can_hide: permissions.dig(:hide, post_id) || PostPermissionService.can_hide?(post, current_user),
  316. can_comment: permissions.dig(:comment, post_id) || PostPermissionService.can_comment?(post, current_user)
  317. }
  318. end
  319. # 批量获取点赞状态
  320. def self.batch_get_like_statuses(posts, current_user)
  321. return {} unless posts.any? && current_user
  322. post_ids = posts.map(&:id)
  323. # 假设有Like模型,批量查询用户对帖子的点赞状态
  324. if defined?(Like)
  325. likes = Like.where(user: current_user, target_id: post_ids, target_type: 'Post')
  326. likes.index_by(&:target_id).transform_values { true }
  327. else
  328. {}
  329. end
  330. rescue
  331. {}
  332. end
  333. # 批量获取收藏状态
  334. def self.batch_get_bookmark_statuses(posts, current_user)
  335. return {} unless posts.any? && current_user
  336. post_ids = posts.map(&:id)
  337. # 假设有Bookmark模型,批量查询用户对帖子的收藏状态
  338. # 如果没有Bookmark模型,返回空哈希
  339. {}
  340. rescue
  341. {}
  342. end
  343. # 类方法:快速格式化
  344. def self.format_post(post, current_user: nil, options: {})
  345. service = new(post: post, current_user: current_user, options: options)
  346. service.call
  347. service.instance_variable_get(:@data)
  348. end
  349. # 类方法:批量格式化
  350. def self.format_posts(posts, current_user: nil, options = {})
  351. posts.map do |post|
  352. format_post(post, current_user: current_user, options: options)
  353. end
  354. end
  355. # 批量格式化帖子 - 优化列表页面性能
  356. def self.batch_format_posts(posts, current_user: nil, options = {})
  357. return [] if posts.blank?
  358. # 预加载关联数据避免N+1查询
  359. posts = posts.includes(:user, :tags, :likes) if posts.respond_to?(:includes)
  360. # 如果有当前用户,批量获取权限信息
  361. permissions = {}
  362. if current_user && posts.any?
  363. post_ids = posts.map(&:id)
  364. permissions = PostPermissionService.batch_check_posts_permissions(
  365. post_ids,
  366. current_user.id,
  367. [:edit, :delete, :pin, :hide, :comment]
  368. )
  369. end
  370. # 批量处理帖子数据
  371. posts.map do |post|
  372. format_post_with_permissions(post, current_user: current_user, options: options, permissions: permissions)
  373. end
  374. end
  375. # 批量生成帖子交互状态 - 权限优化版本
  376. def self.batch_generate_interaction_states(posts, current_user)
  377. return {} unless current_user && posts.any?
  378. post_ids = posts.map(&:id)
  379. # 批量获取权限信息
  380. permissions = PostPermissionService.batch_check_posts_permissions(
  381. post_ids,
  382. current_user.id,
  383. [:edit, :delete, :pin, :hide, :comment]
  384. )
  385. # 批量获取点赞和收藏状态
  386. post_like_statuses = batch_get_like_statuses(posts, current_user)
  387. post_bookmark_statuses = batch_get_bookmark_statuses(posts, current_user)
  388. posts.map do |post|
  389. post_id = post.id
  390. {
  391. post_id: post_id,
  392. liked: post_like_statuses[post_id] || false,
  393. bookmarked: post_bookmark_statuses[post_id] || false,
  394. can_edit: permissions.dig(:edit, post_id) || false,
  395. can_delete: permissions.dig(:delete, post_id) || false,
  396. can_pin: permissions.dig(:pin, post_id) || false,
  397. can_hide: permissions.dig(:hide, post_id) || false,
  398. can_comment: permissions.dig(:comment, post_id) || false
  399. }
  400. end
  401. end
  402. end

app/services/post_management_service.rb

0.0% lines covered

103 relevant lines. 0 lines covered and 103 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostManagementService - 帖子管理服务(重构版)
  3. # 作为帖子相关服务的协调器,提供统一的接口
  4. class PostManagementService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :post, :user, :action, :params
  7. def initialize(post: nil, user:, action:, params: {})
  8. super()
  9. @post = post
  10. @user = user
  11. @action = action
  12. @params = params
  13. end
  14. # 主要调用方法
  15. def call
  16. handle_errors do
  17. validate_params
  18. execute_action
  19. end
  20. self
  21. end
  22. # 类方法:创建帖子
  23. def self.create_post!(user, params)
  24. new(user: user, action: :create, params: params).call
  25. end
  26. # 类方法:更新帖子
  27. def self.update_post!(post, user, params)
  28. new(post: post, user: user, action: :update, params: params).call
  29. end
  30. # 类方法:删除帖子
  31. def self.delete_post!(post, user)
  32. new(post: post, user: user, action: :delete).call
  33. end
  34. # 类方法:置顶帖子
  35. def self.pin_post!(post, user)
  36. new(post: post, user: user, action: :pin).call
  37. end
  38. # 类方法:取消置顶帖子
  39. def self.unpin_post!(post, user)
  40. new(post: post, user: user, action: :unpin).call
  41. end
  42. # 类方法:隐藏帖子
  43. def self.hide_post!(post, user, reason: nil)
  44. new(post: post, user: user, action: :hide, params: { reason: reason }).call
  45. end
  46. # 类方法:显示帖子
  47. def self.unhide_post!(post, user)
  48. new(post: post, user: user, action: :unhide).call
  49. end
  50. # 类方法:获取帖子数据
  51. def self.get_post_data(post, current_user: nil, options: {})
  52. PostDataService.format_post(post, current_user: current_user, options: options)
  53. end
  54. # 类方法:检查权限
  55. def self.check_permission(post, user, action)
  56. PostPermissionService.can_perform?(post, user, action)
  57. end
  58. private
  59. # 验证参数
  60. def validate_params
  61. return failure!("用户不能为空") unless user
  62. return failure!("用户不存在") unless user.persisted?
  63. case action
  64. when :create
  65. return failure!("创建参数不能为空") if params.blank?
  66. when :update, :delete, :pin, :unpin, :hide, :unhide
  67. return failure!("帖子不能为空") unless post
  68. return failure!("帖子不存在") unless post.persisted?
  69. else
  70. return failure!("不支持的操作: #{action}")
  71. end
  72. true
  73. end
  74. # 执行具体操作
  75. def execute_action
  76. result = case action
  77. when :create
  78. create_post
  79. when :update
  80. update_post
  81. when :delete
  82. delete_post
  83. when :pin
  84. moderate_post(:pin)
  85. when :unpin
  86. moderate_post(:unpin)
  87. when :hide
  88. moderate_post(:hide, reason: params[:reason])
  89. when :unhide
  90. moderate_post(:unhide)
  91. else
  92. failure!("不支持的操作: #{action}")
  93. end
  94. if result&.success?
  95. # 从子服务的结果中获取数据
  96. service_data = result.instance_variable_get(:@data)
  97. success!(service_data)
  98. else
  99. failure!(result&.errors || ["操作失败"])
  100. end
  101. end
  102. # 创建帖子
  103. def create_post
  104. PostCreationService.new(user: user, post_params: params).call
  105. end
  106. # 更新帖子
  107. def update_post
  108. PostUpdateService.new(post: post, user: user, post_params: params).call
  109. end
  110. # 删除帖子
  111. def delete_post
  112. PostModerationService.new(post: post, user: user, action: :delete).call
  113. end
  114. # 管理操作(置顶、隐藏等)
  115. def moderate_post(moderation_action, reason: nil)
  116. PostModerationService.new(
  117. post: post,
  118. user: user,
  119. action: moderation_action,
  120. reason: reason
  121. ).call
  122. end
  123. end

app/services/post_moderation_service.rb

0.0% lines covered

157 relevant lines. 0 lines covered and 157 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostModerationService - 帖子管理服务
  3. # 专门负责帖子的管理操作,包括置顶、隐藏、删除等
  4. class PostModerationService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :post, :user, :action, :reason
  7. def initialize(post:, user:, action:, reason: nil)
  8. super()
  9. @post = post
  10. @user = user
  11. @action = action
  12. @reason = reason
  13. end
  14. # 执行管理操作
  15. def call
  16. handle_errors do
  17. validate_moderation_params
  18. check_moderation_permission
  19. execute_moderation_action
  20. process_moderation_result
  21. format_success_response
  22. end
  23. self
  24. end
  25. private
  26. # 验证管理参数
  27. def validate_moderation_params
  28. return failure!("帖子不能为空") unless post
  29. return failure!("用户不能为空") unless user
  30. return failure!("帖子不存在") unless post.persisted?
  31. return failure!("用户不存在") unless user.persisted?
  32. valid_actions = [:pin, :unpin, :hide, :unhide, :delete]
  33. unless valid_actions.include?(action)
  34. return failure!("不支持的管理操作: #{action}")
  35. end
  36. end
  37. # 检查管理权限
  38. def check_moderation_permission
  39. case action
  40. when :pin, :unpin
  41. unless post.can_pin?(user)
  42. failure!("无权限置顶此帖子")
  43. return false
  44. end
  45. when :hide, :unhide
  46. unless post.can_hide?(user)
  47. failure!("无权限隐藏此帖子")
  48. return false
  49. end
  50. when :delete
  51. unless post.can_edit?(user)
  52. failure!("无权限删除此帖子")
  53. return false
  54. end
  55. end
  56. true
  57. end
  58. # 执行管理操作
  59. def execute_moderation_action
  60. case action
  61. when :pin
  62. post.pin!
  63. when :unpin
  64. post.unpin!
  65. when :hide
  66. post.hide!
  67. when :unhide
  68. post.unhide!
  69. when :delete
  70. post.destroy!
  71. end
  72. true
  73. rescue => e
  74. Rails.logger.error "Post moderation error: #{e.message}"
  75. failure!("管理操作失败: #{e.message}")
  76. false
  77. end
  78. # 处理管理操作结果
  79. def process_moderation_result
  80. # 记录管理操作日志
  81. log_moderation_event
  82. # 发送相关通知
  83. send_moderation_notifications
  84. # 更新统计信息(如果需要)
  85. update_statistics
  86. # 清理缓存
  87. clear_cache
  88. end
  89. # 记录管理操作日志
  90. def log_moderation_event
  91. action_text = case action
  92. when :pin then "置顶"
  93. when :unpin then "取消置顶"
  94. when :hide then "隐藏"
  95. when :unhide then "显示"
  96. when :delete then "删除"
  97. end
  98. log_message = "Post #{action_text}: ID #{post.id} by User #{user.id}"
  99. log_message += " - Reason: #{reason}" if reason.present?
  100. Rails.logger.info log_message
  101. end
  102. # 发送管理操作通知
  103. def send_moderation_notifications
  104. case action
  105. when :pin
  106. # 通知帖子作者帖子被置顶
  107. send_pin_notification
  108. when :hide
  109. # 通知帖子作者帖子被隐藏
  110. send_hide_notification
  111. when :delete
  112. # 通知帖子作者帖子被删除
  113. send_delete_notification
  114. end
  115. end
  116. # 发送置顶通知
  117. def send_pin_notification
  118. return if user.id == post.user_id # 自己操作自己不通知
  119. # NotificationService.post_pinned_notification(post, user)
  120. end
  121. # 发送隐藏通知
  122. def send_hide_notification
  123. return if user.id == post.user_id
  124. # NotificationService.post_hidden_notification(post, user, reason)
  125. end
  126. # 发送删除通知
  127. def send_delete_notification
  128. return if user.id == post.user_id
  129. # NotificationService.post_deleted_notification(post, user, reason)
  130. end
  131. # 更新统计信息
  132. def update_statistics
  133. case action
  134. when :pin
  135. # 更新置顶帖子统计
  136. update_pin_statistics
  137. when :hide
  138. # 更新隐藏帖子统计
  139. update_hide_statistics
  140. when :delete
  141. # 更新删除统计
  142. update_delete_statistics
  143. end
  144. end
  145. # 更新置顶统计
  146. def update_pin_statistics
  147. # 统计逻辑 - 检查字段是否存在
  148. if post.user.respond_to?(:pinned_posts_count)
  149. if action == :pin
  150. post.user.increment!(:pinned_posts_count)
  151. else
  152. post.user.decrement!(:pinned_posts_count)
  153. end
  154. end
  155. end
  156. # 更新隐藏统计
  157. def update_hide_statistics
  158. # 统计逻辑
  159. end
  160. # 更新删除统计
  161. def update_delete_statistics
  162. # 更新用户帖子数量 - 检查字段是否存在
  163. post.user.decrement!(:posts_count) if post.user.respond_to?(:posts_count)
  164. end
  165. # 清理缓存
  166. def clear_cache
  167. # 清理帖子相关的缓存
  168. Rails.cache.delete("post_#{post.id}")
  169. Rails.cache.delete("user_posts_#{post.user_id}")
  170. Rails.cache.delete("posts_list")
  171. end
  172. # 格式化成功响应
  173. def format_success_response
  174. action_text = case action
  175. when :pin then "置顶"
  176. when :unpin then "取消置顶"
  177. when :hide then "隐藏"
  178. when :unhide then "显示"
  179. when :delete then "删除"
  180. end
  181. response_data = {
  182. message: "帖子#{action_text}成功"
  183. }
  184. # 对于非删除操作,返回更新后的帖子数据
  185. unless action == :delete
  186. response_data[:post] = post_data(post)
  187. end
  188. success!(response_data)
  189. end
  190. # 格式化帖子数据
  191. def post_data(post)
  192. return nil if action == :delete
  193. post.as_json_for_api(current_user: user)
  194. end
  195. end

app/services/post_permission_service.rb

0.0% lines covered

186 relevant lines. 0 lines covered and 186 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostPermissionService - 帖子权限检查服务
  3. # 专门负责帖子相关操作的权限验证逻辑
  4. class PostPermissionService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :post, :user, :action
  7. def initialize(post:, user:, action:)
  8. super()
  9. @post = post
  10. @user = user
  11. @action = action
  12. end
  13. # 检查权限
  14. def call
  15. handle_errors do
  16. validate_permission_params
  17. check_specific_permission
  18. end
  19. self
  20. end
  21. # 快速权限检查方法(不使用handle_errors包装)
  22. def can_perform?
  23. validate_permission_params && check_specific_permission
  24. rescue
  25. false
  26. end
  27. private
  28. # 验证权限检查参数
  29. def validate_permission_params
  30. return failure!("用户不能为空") unless user
  31. return failure!("帖子不能为空") unless post
  32. return failure!("用户不存在") unless user.persisted?
  33. return failure!("帖子不存在") unless post.persisted?
  34. valid_actions = [:edit, :delete, :pin, :hide, :view, :comment]
  35. unless valid_actions.include?(action)
  36. return failure!("不支持的权限检查操作: #{action}")
  37. end
  38. true
  39. end
  40. # 检查具体权限
  41. def check_specific_permission
  42. result = case action
  43. when :edit
  44. can_edit?
  45. when :delete
  46. can_delete?
  47. when :pin
  48. can_pin?
  49. when :hide
  50. can_hide?
  51. when :view
  52. can_view?
  53. when :comment
  54. can_comment?
  55. else
  56. false
  57. end
  58. if result
  59. success!("权限检查通过")
  60. else
  61. failure!("权限不足")
  62. end
  63. end
  64. # 检查编辑权限
  65. def can_edit?
  66. # 帖子作者可以编辑自己的帖子
  67. return true if post.user_id == user.id
  68. # 管理员可以编辑任何帖子
  69. return true if user.admin?
  70. # 超级管理员可以编辑任何帖子
  71. return true if user.super_admin?
  72. false
  73. end
  74. # 检查删除权限
  75. def can_delete?
  76. # 帖子作者可以删除自己的帖子
  77. return true if post.user_id == user.id
  78. # 管理员可以删除任何帖子
  79. return true if user.admin?
  80. # 超级管理员可以删除任何帖子
  81. return true if user.super_admin?
  82. false
  83. end
  84. # 检查置顶权限
  85. def can_pin?
  86. # 只有管理员和超级管理员可以置顶帖子
  87. return true if user.admin?
  88. return true if user.super_admin?
  89. false
  90. end
  91. # 检查隐藏权限
  92. def can_hide?
  93. # 管理员和超级管理员可以隐藏帖子
  94. return true if user.admin?
  95. return true if user.super_admin?
  96. false
  97. end
  98. # 检查查看权限
  99. def can_view?
  100. # 已删除的帖子只有作者和管理员可以查看
  101. if post.deleted?
  102. return post.user_id == user.id || user.admin? || user.super_admin?
  103. end
  104. # 隐藏的帖子只有作者和管理员可以查看
  105. if post.hidden?
  106. return post.user_id == user.id || user.admin? || user.super_admin?
  107. end
  108. # 公开帖子所有人都可以查看
  109. true
  110. end
  111. # 检查评论权限
  112. def can_comment?
  113. # 不能对已删除的帖子评论
  114. return false if post.deleted?
  115. # 不能对隐藏的帖子评论(除非是作者或管理员)
  116. if post.hidden?
  117. return post.user_id == user.id || user.admin? || user.super_admin?
  118. end
  119. # 其他情况都可以评论
  120. true
  121. end
  122. # 类方法:快速权限检查
  123. def self.can_edit?(post, user)
  124. new(post: post, user: user, action: :edit).can_perform?
  125. end
  126. def self.can_delete?(post, user)
  127. new(post: post, user: user, action: :delete).can_perform?
  128. end
  129. def self.can_pin?(post, user)
  130. new(post: post, user: user, action: :pin).can_perform?
  131. end
  132. def self.can_hide?(post, user)
  133. new(post: post, user: user, action: :hide).can_perform?
  134. end
  135. def self.can_view?(post, user)
  136. new(post: post, user: user, action: :view).can_perform?
  137. end
  138. def self.can_comment?(post, user)
  139. new(post: post, user: user, action: :comment).can_perform?
  140. end
  141. # 带缓存的权限检查方法
  142. def self.can_edit_cached?(post, user, cache_options = {})
  143. can_perform_cached?(:edit, post, user, cache_options)
  144. end
  145. def self.can_delete_cached?(post, user, cache_options = {})
  146. can_perform_cached?(:delete, post, user, cache_options)
  147. end
  148. def self.can_pin_cached?(post, user, cache_options = {})
  149. can_perform_cached?(:pin, post, user, cache_options)
  150. end
  151. def self.can_hide_cached?(post, user, cache_options = {})
  152. can_perform_cached?(:hide, post, user, cache_options)
  153. end
  154. def self.can_view_cached?(post, user, cache_options = {})
  155. can_perform_cached?(:view, post, user, cache_options)
  156. end
  157. def self.can_comment_cached?(post, user, cache_options = {})
  158. can_perform_cached?(:comment, post, user, cache_options)
  159. end
  160. # 批量权限检查 - 优化列表页面性能
  161. def self.batch_check_posts_permissions(post_ids, user_id, actions = [:edit, :delete, :pin, :hide, :comment])
  162. return {} if post_ids.blank? || user_id.blank?
  163. cache_keys = actions.product(post_ids).map do |action, post_id|
  164. "post_permission:#{action}:#{post_id}:#{user_id}"
  165. end
  166. # 尝试从缓存获取
  167. cached_results = Rails.cache.read_multi(*cache_keys)
  168. # 找出需要查询的权限
  169. uncached_permissions = []
  170. actions.each do |action|
  171. post_ids.each do |post_id|
  172. cache_key = "post_permission:#{action}:#{post_id}:#{user_id}"
  173. unless cached_results.key?(cache_key)
  174. uncached_permissions << { action: action, post_id: post_id, cache_key: cache_key }
  175. end
  176. end
  177. end
  178. # 批量查询并缓存未缓存的权限
  179. if uncached_permissions.any?
  180. batch_cache_permissions(uncached_permissions, user_id)
  181. end
  182. # 组织返回结果
  183. results = {}
  184. actions.each do |action|
  185. results[action] = {}
  186. post_ids.each do |post_id|
  187. cache_key = "post_permission:#{action}:#{post_id}:#{user_id}"
  188. results[action][post_id.to_i] = cached_results[cache_key] || false
  189. end
  190. end
  191. results
  192. end
  193. private
  194. # 通用缓存权限检查方法
  195. def self.can_perform_cached?(action, post, user, cache_options = {})
  196. return false unless post&.persisted? && user&.persisted?
  197. cache_key = "post_permission:#{action}:#{post.id}:#{user.id}"
  198. cache_options = {
  199. expires_in: cache_options[:expires_in] || 5.minutes,
  200. race_condition_ttl: 10.seconds
  201. }.merge(cache_options)
  202. Rails.cache.fetch(cache_key, cache_options) do
  203. new(post: post, user: user, action: action).can_perform?
  204. end
  205. end
  206. # 批量缓存权限检查结果
  207. def self.batch_cache_permissions(permissions, user_id)
  208. # 按action分组以优化数据库查询
  209. permissions_by_action = permissions.group_by { |p| p[:action] }
  210. permissions_by_action.each do |action, perms|
  211. post_ids = perms.map { |p| p[:post_id] }
  212. # 批量加载帖子和用户
  213. posts = Post.where(id: post_ids).includes(:user)
  214. user = User.find_by(id: user_id)
  215. next unless user
  216. # 批量检查权限
  217. perms.each do |perm|
  218. post = posts.find { |p| p.id == perm[:post_id] }
  219. next unless post
  220. result = new(post: post, user: user, action: action).can_perform?
  221. Rails.cache.write(perm[:cache_key], result, expires_in: 5.minutes)
  222. end
  223. end
  224. end
  225. end

app/services/post_service_facade.rb

0.0% lines covered

158 relevant lines. 0 lines covered and 158 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostServiceFacade - 帖子服务门面
  3. # 简化控制器层对帖子服务的调用,提供统一的接口
  4. class PostServiceFacade < ApplicationService
  5. include ServiceInterface
  6. attr_reader :user, :current_user, :params
  7. def initialize(user:, current_user: nil, params: {})
  8. super()
  9. @user = user
  10. @current_user = current_user || user
  11. @params = params
  12. end
  13. def call
  14. handle_errors do
  15. validate_parameters
  16. execute_operation
  17. end
  18. self
  19. end
  20. # 类方法:创建帖子并返回格式化数据
  21. def self.create_with_data(user, params, current_user: nil)
  22. new(user: user, current_user: current_user, params: params).tap do |facade|
  23. facade.instance_variable_set(:@action, :create)
  24. facade.call
  25. end
  26. end
  27. # 类方法:更新帖子并返回格式化数据
  28. def self.update_with_data(post, user, params, current_user: nil)
  29. facade = new(user: user, current_user: current_user, params: params)
  30. facade.instance_variable_set(:@post, post)
  31. facade.instance_variable_set(:@action, :update)
  32. facade.call
  33. facade
  34. end
  35. # 类方法:删除帖子
  36. def self.delete_post(post, user, current_user: nil)
  37. facade = new(user: user, current_user: current_user)
  38. facade.instance_variable_set(:@post, post)
  39. facade.instance_variable_set(:@action, :delete)
  40. facade.call
  41. facade
  42. end
  43. # 类方法:置顶帖子
  44. def self.pin_post(post, user, current_user: nil)
  45. facade = new(user: user, current_user: current_user)
  46. facade.instance_variable_set(:@post, post)
  47. facade.instance_variable_set(:@action, :pin)
  48. facade.call
  49. facade
  50. end
  51. # 类方法:取消置顶帖子
  52. def self.unpin_post(post, user, current_user: nil)
  53. facade = new(user: user, current_user: current_user)
  54. facade.instance_variable_set(:@post, post)
  55. facade.instance_variable_set(:@action, :unpin)
  56. facade.call
  57. facade
  58. end
  59. private
  60. def validate_parameters
  61. case action
  62. when :create
  63. errors.add(:user, "用户不能为空") if user.blank?
  64. errors.add(:params, "创建参数不能为空") if params.blank?
  65. errors.add(:title, "标题不能为空") if params[:title].blank?
  66. errors.add(:content, "内容不能为空") if params[:content].blank?
  67. when :update, :delete, :pin, :unpin
  68. post = instance_variable_get(:@post)
  69. errors.add(:post, "帖子不能为空") if post.blank?
  70. errors.add(:user, "用户不能为空") if user.blank?
  71. end
  72. end
  73. def execute_operation
  74. case action
  75. when :create
  76. create_post_with_data
  77. when :update
  78. update_post_with_data
  79. when :delete
  80. delete_post_action
  81. when :pin, :unpin
  82. moderate_post_action(action)
  83. else
  84. failure!("不支持的操作")
  85. end
  86. end
  87. def create_post_with_data
  88. # 使用PostCreationService创建帖子
  89. creation_result = PostCreationService.new(user: user, post_params: params).call
  90. unless creation_result.success?
  91. return failure!(creation_result.error_messages)
  92. end
  93. post = creation_result.data[:post]
  94. # 发布帖子创建事件
  95. DomainEventsService.publish('post.created', {
  96. post: post,
  97. user: user
  98. })
  99. # 格式化帖子数据
  100. formatted_data = PostDataService.format_post(post, current_user: current_user)
  101. success!({
  102. post: formatted_data,
  103. message: "帖子创建成功"
  104. })
  105. end
  106. def update_post_with_data
  107. post = instance_variable_get(:@post)
  108. # 使用PostUpdateService更新帖子
  109. update_result = PostUpdateService.new(
  110. post: post,
  111. user: user,
  112. post_params: params
  113. ).call
  114. unless update_result.success?
  115. return failure!(update_result.error_messages)
  116. end
  117. # 发布帖子更新事件
  118. DomainEventsService.publish('post.updated', {
  119. post: post,
  120. user: user
  121. })
  122. # 格式化帖子数据
  123. formatted_data = PostDataService.format_post(post, current_user: current_user)
  124. success!({
  125. post: formatted_data,
  126. message: "帖子更新成功"
  127. })
  128. end
  129. def delete_post_action
  130. post = instance_variable_get(:@post)
  131. # 使用PostModerationService删除帖子
  132. deletion_result = PostModerationService.new(
  133. post: post,
  134. user: user,
  135. action: :delete
  136. ).call
  137. if deletion_result.success?
  138. # 发布帖子审核事件
  139. DomainEventsService.publish('post.moderated', {
  140. post: post,
  141. moderator: user,
  142. action: :delete,
  143. reason: params[:reason]
  144. })
  145. success!({ message: "帖子删除成功" })
  146. else
  147. failure!(deletion_result.error_messages)
  148. end
  149. end
  150. def moderate_post_action(moderation_action)
  151. post = instance_variable_get(:@post)
  152. # 使用PostModerationService进行管理操作
  153. moderation_result = PostModerationService.new(
  154. post: post,
  155. user: user,
  156. action: moderation_action,
  157. reason: params[:reason]
  158. ).call
  159. if moderation_result.success?
  160. # 发布帖子审核事件
  161. DomainEventsService.publish('post.moderated', {
  162. post: post,
  163. moderator: user,
  164. action: moderation_action,
  165. reason: params[:reason]
  166. })
  167. action_name = moderation_action == :pin ? "置顶" : "取消置顶"
  168. success!({ message: "帖子#{action_name}成功" })
  169. else
  170. failure!(moderation_result.error_messages)
  171. end
  172. end
  173. def action
  174. @action || :create
  175. end
  176. end

app/services/post_update_service.rb

0.0% lines covered

88 relevant lines. 0 lines covered and 88 lines missed.
    
  1. # frozen_string_literal: true
  2. # PostUpdateService - 帖子更新服务
  3. # 专门负责帖子的更新逻辑,包括权限验证、内容更新等
  4. class PostUpdateService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :post, :user, :post_params
  7. def initialize(post:, user:, post_params:)
  8. super()
  9. @post = post
  10. @user = user
  11. @post_params = post_params
  12. end
  13. # 更新帖子
  14. def call
  15. handle_errors do
  16. validate_update_params
  17. check_edit_permission
  18. update_post
  19. process_post_update
  20. format_success_response
  21. end
  22. self
  23. end
  24. private
  25. # 验证更新参数
  26. def validate_update_params
  27. return failure!("帖子不能为空") unless post
  28. return failure!("用户不能为空") unless user
  29. return failure!("帖子不存在") unless post.persisted?
  30. return failure!("用户不存在") unless user.persisted?
  31. # 验证内容长度(如果提供)
  32. if post_params[:content].present?
  33. if post_params[:content].length < 10
  34. return failure!("内容长度不能少于10个字符")
  35. end
  36. if post_params[:content].length > 10000
  37. return failure!("内容长度不能超过10000个字符")
  38. end
  39. end
  40. # 验证标题长度(如果提供)
  41. if post_params[:title].present?
  42. if post_params[:title].length > 100
  43. return failure!("标题长度不能超过100个字符")
  44. end
  45. end
  46. end
  47. # 检查编辑权限
  48. def check_edit_permission
  49. unless post.can_edit?(user)
  50. failure!("无权限编辑此帖子")
  51. return false
  52. end
  53. true
  54. end
  55. # 更新帖子
  56. def update_post
  57. unless post.update(post_params)
  58. failure!(post.errors.full_messages)
  59. return false
  60. end
  61. true
  62. end
  63. # 处理帖子更新后的逻辑
  64. def process_post_update
  65. # 处理标签更新
  66. process_tags_update if post_params[:tags].present?
  67. # 处理图片更新
  68. process_images_update if post_params[:images].present?
  69. # 记录更新日志
  70. log_update_event
  71. # 发送更新通知(如果需要)
  72. send_update_notifications
  73. end
  74. # 处理标签更新
  75. def process_tags_update
  76. tags = post_params[:tags].map(&:strip).reject(&:blank?).uniq
  77. post.update!(tags: tags)
  78. end
  79. # 处理图片更新
  80. def process_images_update
  81. valid_images = post_params[:images].select { |url| valid_image_url?(url) }
  82. post.update!(images: valid_images)
  83. end
  84. # 验证图片URL
  85. def valid_image_url?(url)
  86. uri = URI.parse(url)
  87. uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
  88. rescue URI::InvalidURIError
  89. false
  90. end
  91. # 记录更新事件
  92. def log_update_event
  93. Rails.logger.info "Post updated: ID #{post.id} by User #{user.id}"
  94. end
  95. # 发送更新通知
  96. def send_update_notifications
  97. # 这里可以添加通知逻辑,比如通知关注者有更新
  98. # NotificationService.post_updated_notification(post)
  99. end
  100. # 格式化成功响应
  101. def format_success_response
  102. success!({
  103. message: "帖子更新成功",
  104. post: post_data(post)
  105. })
  106. end
  107. # 格式化帖子数据
  108. def post_data(post)
  109. post.as_json_for_api(current_user: user)
  110. end
  111. end

app/services/query_cache_service.rb

0.0% lines covered

217 relevant lines. 0 lines covered and 217 lines missed.
    
  1. # frozen_string_literal: true
  2. # QueryCacheService - 查询缓存服务
  3. # 提供多层缓存策略:内存缓存、Redis缓存、查询结果缓存
  4. class QueryCacheService < ApplicationService
  5. include ServiceInterface
  6. # 缓存层级
  7. MEMORY_CACHE = {}
  8. CACHE_LOCKS = {}
  9. attr_reader :cache_key, :cache_options, :fallback_proc, :cache_level
  10. def initialize(cache_key:, cache_options: {}, fallback_proc: nil, cache_level: :redis)
  11. super()
  12. @cache_key = cache_key
  13. @cache_options = default_cache_options.merge(cache_options)
  14. @fallback_proc = fallback_proc
  15. @cache_level = cache_level
  16. end
  17. def call
  18. handle_errors do
  19. validate_parameters
  20. fetch_with_cache
  21. end
  22. self
  23. end
  24. # 类方法:缓存查询结果
  25. def self.fetch(cache_key, cache_options: {}, cache_level: :redis, &block)
  26. new(
  27. cache_key: cache_key,
  28. cache_options: cache_options,
  29. fallback_proc: block,
  30. cache_level: cache_level
  31. ).call.data
  32. end
  33. # 类方法:缓存帖子列表
  34. def self.fetch_posts_list(filters = {}, page: 1, per_page: 20, current_user: nil)
  35. cache_key = "posts_list:#{filters.to_query_hash}:#{page}:#{per_page}:#{current_user&.id}"
  36. fetch(cache_key,
  37. expires_in: 5.minutes,
  38. cache_level: :redis) do
  39. # 构建查询
  40. posts = Post.visible.includes(:user)
  41. .order(pinned: :desc, created_at: :desc)
  42. # 应用筛选条件
  43. posts = posts.by_category(filters[:category]) if filters[:category].present?
  44. # 分页
  45. posts = posts.limit(per_page).offset((page - 1) * per_page)
  46. # 预加载权限和点赞状态
  47. if current_user
  48. post_ids = posts.map(&:id)
  49. permissions = PostPermissionService.batch_check_posts_permissions(
  50. post_ids, current_user.id
  51. )
  52. liked_post_ids = Like.where(
  53. user_id: current_user.id,
  54. target_type: 'Post',
  55. target_id: post_ids
  56. ).pluck(:target_id)
  57. posts.each do |post|
  58. post.instance_variable_set(:@permissions, permissions)
  59. post.instance_variable_set(:@current_user_liked, liked_post_ids.include?(post.id))
  60. end
  61. end
  62. posts
  63. end
  64. end
  65. # 类方法:缓存单个帖子
  66. def self.fetch_post(post_id, current_user: nil)
  67. cache_key = "post:#{post_id}:#{current_user&.id}"
  68. fetch(cache_key,
  69. expires_in: 10.minutes,
  70. cache_level: :redis) do
  71. post = Post.includes(:user).find(post_id)
  72. # 预加载权限和点赞状态
  73. if current_user
  74. permissions = PostPermissionService.batch_check_posts_permissions(
  75. [post_id], current_user.id
  76. )
  77. liked = Like.exists?(
  78. user_id: current_user.id,
  79. target_type: 'Post',
  80. target_id: post_id
  81. )
  82. post.instance_variable_set(:@permissions, permissions)
  83. post.instance_variable_set(:@current_user_liked, liked)
  84. end
  85. post
  86. end
  87. end
  88. # 类方法:缓存用户统计
  89. def self.fetch_user_stats(user_id)
  90. cache_key = "user_stats:#{user_id}"
  91. fetch(cache_key,
  92. expires_in: 1.hour,
  93. cache_level: :redis) do
  94. user = User.find(user_id)
  95. {
  96. posts_count: user.posts_count,
  97. comments_count: user.comments_count,
  98. flowers_given_count: user.flowers_given_count,
  99. flowers_received_count: user.flowers_received_count,
  100. likes_given_count: user.likes_given_count
  101. }
  102. end
  103. end
  104. # 类方法:缓存活动统计
  105. def self.fetch_event_stats(event_id)
  106. cache_key = "event_stats:#{event_id}"
  107. fetch(cache_key,
  108. expires_in: 30.minutes,
  109. cache_level: :redis) do
  110. event = ReadingEvent.find(event_id)
  111. {
  112. enrollments_count: event.enrollments_count,
  113. check_ins_count: event.check_ins_count,
  114. flowers_count: event.flowers_count,
  115. completion_rate: calculate_completion_rate(event)
  116. }
  117. end
  118. end
  119. # 类方法:清除缓存
  120. def self.clear_cache(pattern = nil)
  121. if pattern
  122. # 清除匹配模式的缓存
  123. if defined?(Rails) && Rails.cache.respond_to?(:delete_matched)
  124. Rails.cache.delete_matched(pattern)
  125. end
  126. # 清除内存缓存
  127. MEMORY_CACHE.delete_if { |key, _| key.match?(Regexp.new(pattern)) }
  128. else
  129. # 清除所有缓存
  130. if defined?(Rails) && Rails.cache.respond_to?(:clear)
  131. Rails.cache.clear
  132. end
  133. MEMORY_CACHE.clear
  134. end
  135. end
  136. # 类方法:预热缓存
  137. def self.warmup_popular_data
  138. # 预热热门帖子
  139. popular_posts = Post.visible.order(likes_count: :desc).limit(10)
  140. popular_posts.each do |post|
  141. fetch_post(post.id)
  142. end
  143. # 预热活动统计
  144. active_events = ReadingEvent.where(status: :active).limit(5)
  145. active_events.each do |event|
  146. fetch_event_stats(event.id)
  147. end
  148. Rails.logger.info "缓存预热完成"
  149. end
  150. def data
  151. @data
  152. end
  153. def cache_hit?
  154. @cache_hit
  155. end
  156. private
  157. def validate_parameters
  158. errors.add(:cache_key, "缓存键不能为空") if cache_key.blank?
  159. errors.add(:fallback_proc, "必须提供fallback_proc或代码块") if fallback_proc.nil?
  160. end
  161. def fetch_with_cache
  162. # 尝试从缓存获取
  163. cached_value = get_from_cache
  164. if cached_value.present?
  165. @cache_hit = true
  166. @data = cached_value
  167. Rails.logger.debug "缓存命中: #{cache_key}"
  168. return self
  169. end
  170. # 防止缓存击穿
  171. @cache_hit = false
  172. @data = fetch_with_lock
  173. # 存入缓存
  174. set_to_cache(@data)
  175. Rails.logger.debug "缓存未命中,已设置: #{cache_key}"
  176. self
  177. end
  178. def fetch_with_lock
  179. # 使用分布式锁防止缓存击穿
  180. lock_key = "cache_lock:#{cache_key}"
  181. if cache_level == :redis && defined?(Rails)
  182. # 使用Redis分布式锁
  183. lock_value = SecureRandom.uuid
  184. if Rails.cache.add(lock_key, lock_value, expires_in: 30.seconds)
  185. begin
  186. result = fallback_proc.call
  187. return result
  188. ensure
  189. Rails.cache.delete(lock_key)
  190. end
  191. else
  192. # 等待其他进程完成,然后重试获取缓存
  193. sleep(0.1)
  194. cached_value = get_from_cache
  195. return cached_value if cached_value.present?
  196. end
  197. else
  198. # 使用内存锁
  199. CACHE_LOCKS[cache_key] ||= Mutex.new
  200. CACHE_LOCKS[cache_key].synchronize do
  201. result = fallback_proc.call
  202. return result
  203. end
  204. end
  205. end
  206. def get_from_cache
  207. case cache_level
  208. when :memory
  209. MEMORY_CACHE[cache_key]
  210. when :redis
  211. if defined?(Rails) && Rails.cache
  212. Rails.cache.read(cache_key)
  213. else
  214. nil
  215. end
  216. else
  217. nil
  218. end
  219. end
  220. def set_to_cache(value)
  221. return unless value
  222. case cache_level
  223. when :memory
  224. MEMORY_CACHE[cache_key] = value
  225. when :redis
  226. if defined?(Rails) && Rails.cache
  227. Rails.cache.write(cache_key, value, **cache_options)
  228. end
  229. end
  230. end
  231. def default_cache_options
  232. {
  233. expires_in: 30.minutes,
  234. race_condition_ttl: 30.seconds,
  235. compress: true
  236. }
  237. end
  238. def calculate_completion_rate(event)
  239. return 0 if event.enrollments_count == 0
  240. total_days = (event.end_date - event.start_date).to_i + 1
  241. expected_check_ins = event.enrollments_count * total_days
  242. return 0 if expected_check_ins == 0
  243. (event.check_ins_count.to_f / expected_check_ins * 100).round(2)
  244. end
  245. end

app/services/query_optimization_service.rb

0.0% lines covered

158 relevant lines. 0 lines covered and 158 lines missed.
    
  1. # frozen_string_literal: true
  2. # 查询优化服务
  3. # 提供高性能的数据库查询方法,减少N+1查询和优化复杂查询
  4. class QueryOptimizationService
  5. class << self
  6. # 批量预加载关联数据,避免N+1查询
  7. # @param records [Array] ActiveRecord记录数组
  8. # @param includes [Array] 需要预加载的关联
  9. # @return [Array] 预加载后的记录
  10. def preload_associations(records, includes)
  11. return records if records.empty?
  12. # 使用ActiveRecord的preload方法避免N+1查询
  13. if records.first.is_a?(Class)
  14. # 如果是模型类,使用includes
  15. records.includes(includes)
  16. else
  17. # 如果是记录数组,使用preload
  18. ActiveRecord::Associations::Preloader.new.preload(records, includes)
  19. records
  20. end
  21. end
  22. # 优化的用户查询,包含常用关联
  23. # @param scope [ActiveRecord::Relation] 基础查询范围
  24. # @param options [Hash] 查询选项
  25. # @return [ActiveRecord::Relation] 优化后的查询
  26. def optimized_user_query(scope = User.all, options = {})
  27. includes = [:created_events, :event_enrollments, :check_ins, :comments]
  28. includes << :received_flowers if options[:include_flowers]
  29. includes << :flower_certificates if options[:include_certificates]
  30. scope.includes(includes)
  31. end
  32. # 优化的活动查询,包含统计信息
  33. # @param scope [ActiveRecord::Relation] 基础查询范围
  34. # @param options [Hash] 查询选项
  35. # @return [ActiveRecord::Relation] 优化后的查询
  36. def optimized_event_query(scope = ReadingEvent.all, options = {})
  37. includes = [:leader, :event_enrollments, :reading_schedules]
  38. includes << :check_ins if options[:include_check_ins]
  39. includes << :flowers if options[:include_flowers]
  40. query = scope.includes(includes)
  41. # 如果需要统计数据,使用子查询而不是JOIN
  42. if options[:include_stats]
  43. query = query.select(
  44. 'reading_events.*',
  45. '(SELECT COUNT(*) FROM event_enrollments WHERE event_enrollments.reading_event_id = reading_events.id AND event_enrollments.status = \'enrolled\') as enrolled_count',
  46. '(SELECT COUNT(*) FROM check_ins JOIN reading_schedules ON check_ins.reading_schedule_id = reading_schedules.id WHERE reading_schedules.reading_event_id = reading_events.id) as check_ins_count',
  47. '(SELECT COUNT(*) FROM flowers JOIN check_ins ON flowers.check_in_id = check_ins.id JOIN reading_schedules ON check_ins.reading_schedule_id = reading_schedules.id WHERE reading_schedules.reading_event_id = reading_events.id) as flowers_count'
  48. )
  49. end
  50. query
  51. end
  52. # 优化的打卡查询,包含内容分析
  53. # @param scope [ActiveRecord::Relation] 基础查询范围
  54. # @param options [Hash] 查询选项
  55. # @return [ActiveRecord::Relation] 优化后的查询
  56. def optimized_check_in_query(scope = CheckIn.all, options = {})
  57. includes = [:user, :reading_schedule, :enrollment]
  58. includes << :flowers if options[:include_flowers]
  59. includes << :comments if options[:include_comments]
  60. includes << :reading_event if options[:include_event]
  61. scope.includes(includes)
  62. end
  63. # 优化的通知查询,优先显示未读通知
  64. # @param user [User] 用户对象
  65. # @param options [Hash] 查询选项
  66. # @return [ActiveRecord::Relation] 优化后的查询
  67. def optimized_notification_query(user, options = {})
  68. query = user.received_notifications
  69. # 按未读状态和创建时间排序,未读通知优先
  70. query = query.order(read: :asc, created_at: :desc)
  71. # 包含关联数据
  72. includes = [:actor]
  73. includes << :notifiable if options[:include_notifiable]
  74. query = query.includes(includes)
  75. query
  76. end
  77. # 批量查询优化 - 使用IN查询而不是多次单独查询
  78. # @param model_class [Class] ActiveRecord模型类
  79. # @param ids [Array] ID数组
  80. # @param includes [Array] 需要预加载的关联
  81. # @return [Array] 查询结果
  82. def batch_find_by_ids(model_class, ids, includes = [])
  83. return [] if ids.empty?
  84. # 分批处理,避免IN子句过长
  85. batch_size = 1000
  86. results = []
  87. ids.each_slice(batch_size) do |batch_ids|
  88. query = model_class.where(id: batch_ids)
  89. query = query.includes(includes) if includes.any?
  90. results.concat(query.to_a)
  91. end
  92. # 按原始ID顺序排序
  93. id_index = ids.each_with_index.to_h
  94. results.sort_by { |record| id_index[record.id] }
  95. end
  96. # 优化的排行榜查询,使用窗口函数提高性能
  97. # @param model_class [Class] 模型类
  98. # @param count_column [String] 计数字段名
  99. # @param limit [Integer] 返回记录数限制
  100. # @param includes [Array] 需要预加载的关联
  101. # @return [Array] 排行榜数据
  102. def optimized_leaderboard_query(model_class, count_column, limit = 10, includes = [])
  103. # 使用窗口函数的子查询(如果数据库支持)
  104. if database_supports_window_functions?
  105. sql = <<~SQL
  106. SELECT *,
  107. DENSE_RANK() OVER (ORDER BY #{count_column} DESC, created_at ASC) as rank
  108. FROM #{model_class.table_name}
  109. ORDER BY #{count_column} DESC, created_at ASC
  110. LIMIT ?
  111. SQL
  112. records = model_class.find_by_sql([sql, limit])
  113. else
  114. # 回退到普通查询
  115. records = model_class.order("#{count_column} DESC, created_at ASC")
  116. .limit(limit)
  117. .to_a
  118. # 手动计算排名
  119. records.each_with_index do |record, index|
  120. record.define_singleton_method(:rank) { index + 1 }
  121. end
  122. end
  123. # 预加载关联数据
  124. if includes.any?
  125. ActiveRecord::Associations::Preloader.new.preload(records, includes)
  126. end
  127. records
  128. end
  129. # 优化的计数查询,使用缓存避免重复计算
  130. # @param query [ActiveRecord::Relation] 查询对象
  131. # @param cache_key [String] 缓存键
  132. # @param cache_ttl [Integer] 缓存时间(秒)
  133. # @return [Integer] 计数结果
  134. def optimized_count_query(query, cache_key = nil, cache_ttl = 5.minutes)
  135. if cache_key && Rails.cache.respond_to?(:fetch)
  136. Rails.cache.fetch(cache_key, expires_in: cache_ttl) do
  137. query.count
  138. end
  139. else
  140. query.count
  141. end
  142. end
  143. # 优化的存在性查询,使用EXISTS而不是COUNT
  144. # @param query [ActiveRecord::Relation] 查询对象
  145. # @return [Boolean] 是否存在记录
  146. def optimized_exists_query(query)
  147. query.exists?
  148. end
  149. # 优化的分页查询,使用cursor-based分页提高性能
  150. # @param scope [ActiveRecord::Relation] 基础查询范围
  151. # @param cursor [Integer] 游标位置
  152. # @param limit [Integer] 每页记录数
  153. # @param order_column [String] 排序字段
  154. # @return [Array] 分页结果和下一页游标
  155. def cursor_paginated_query(scope, cursor: nil, limit: 20, order_column: 'id')
  156. query = scope.order(order_column => :asc).limit(limit + 1)
  157. if cursor
  158. query = query.where("#{order_column} > ?", cursor)
  159. end
  160. records = query.to_a
  161. has_next = records.length > limit
  162. next_cursor = has_next ? records.last.send(order_column) : nil
  163. records = records.first(limit)
  164. {
  165. records: records,
  166. next_cursor: next_cursor,
  167. has_next: has_next
  168. }
  169. end
  170. # 批量插入优化,使用批量插入减少数据库往返
  171. # @param model_class [Class] 模型类
  172. # @param attributes_array [Array] 属性数组
  173. # @param batch_size [Integer] 批次大小
  174. # @return [Array] 创建的记录
  175. def batch_insert(model_class, attributes_array, batch_size = 1000)
  176. return [] if attributes_array.empty?
  177. created_records = []
  178. attributes_array.each_slice(batch_size) do |batch|
  179. records = model_class.insert_all(batch, returning: true)
  180. created_records.concat(records)
  181. end
  182. created_records
  183. end
  184. # 优化的统计查询,使用数据库聚合函数
  185. # @param model_class [Class] 模型类
  186. # @param group_column [String] 分组字段
  187. # @param aggregations [Hash] 聚合配置
  188. # @return [Array] 统计结果
  189. def optimized_aggregation_query(model_class, group_column, aggregations)
  190. query = model_class.group(group_column)
  191. aggregations.each do |alias_name, aggregation|
  192. case aggregation[:type]
  193. when :count
  194. query = query.select("#{group_column}, COUNT(*) as #{alias_name}")
  195. when :sum
  196. query = query.select("#{group_column}, SUM(#{aggregation[:column]}) as #{alias_name}")
  197. when :avg
  198. query = query.select("#{group_column}, AVG(#{aggregation[:column]}) as #{alias_name}")
  199. when :max
  200. query = query.select("#{group_column}, MAX(#{aggregation[:column]}) as #{alias_name}")
  201. when :min
  202. query = query.select("#{group_column}, MIN(#{aggregation[:column]}) as #{alias_name}")
  203. end
  204. end
  205. query.to_a
  206. end
  207. private
  208. # 检查数据库是否支持窗口函数
  209. def database_supports_window_functions?
  210. case ActiveRecord::Base.connection.adapter_name.downcase
  211. when 'postgresql', 'mysql'
  212. true
  213. when 'sqlite'
  214. # SQLite 3.25+ 支持窗口函数
  215. sqlite_version = ActiveRecord::Base.connection.select_value("SELECT sqlite_version()")
  216. Gem::Version.new(sqlite_version) >= Gem::Version.new('3.25.0')
  217. else
  218. false
  219. end
  220. end
  221. # 生成查询的缓存键
  222. def generate_cache_key(model_class, query_params = {})
  223. key_parts = [
  224. model_class.name.downcase,
  225. 'query',
  226. Digest::MD5.hexdigest(query_params.to_json)
  227. ]
  228. key_parts.join('_')
  229. end
  230. end
  231. end

app/services/report_creation_service.rb

0.0% lines covered

156 relevant lines. 0 lines covered and 156 lines missed.
    
  1. # frozen_string_literal: true
  2. # ReportCreationService - 举报创建服务
  3. # 专门负责内容举报的创建、验证和初步处理
  4. class ReportCreationService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :user, :target_content, :reason, :description
  7. def initialize(user:, target_content:, reason:, description: nil)
  8. super()
  9. @user = user
  10. @target_content = target_content
  11. @reason = reason
  12. @description = description
  13. end
  14. # 创建举报
  15. def call
  16. handle_errors do
  17. validate_creation_params
  18. check_creation_permissions
  19. validate_report_reason
  20. create_report_record
  21. process_report_creation
  22. format_success_response
  23. end
  24. self
  25. end
  26. private
  27. # 验证创建参数
  28. def validate_creation_params
  29. return failure!("用户不能为空") unless user
  30. return failure!("用户不存在") unless user.persisted?
  31. return failure!("举报内容不能为空") unless target_content
  32. return failure!("举报内容不存在") unless target_content.persisted?
  33. return failure!("举报原因不能为空") unless reason
  34. return failure!("无效的举报原因") unless valid_reason?
  35. true
  36. end
  37. # 检查创建权限
  38. def check_creation_permissions
  39. # 检查是否可以举报此内容
  40. unless can_report_content?
  41. failure!("无权举报此内容或已举报过")
  42. return false
  43. end
  44. true
  45. end
  46. # 验证举报原因
  47. def validate_report_reason
  48. issues = []
  49. # 检查举报原因是否合理
  50. if reason == 'other' && description.blank?
  51. issues << '选择"其他"原因时必须填写描述'
  52. end
  53. # 检查描述长度
  54. if description.present? && description.length < 10
  55. issues << '举报描述太短,请提供更多详细信息'
  56. end
  57. # 检查描述长度上限
  58. if description.present? && description.length > 500
  59. issues << '举报描述不能超过500个字符'
  60. end
  61. # 检查内容是否确实有问题(对敏感词举报进行验证)
  62. if reason == 'sensitive_words'
  63. unless content_has_sensitive_words?
  64. issues << '内容中未检测到敏感词,请确认举报原因'
  65. end
  66. end
  67. if issues.any?
  68. failure!("举报验证失败: #{issues.join(', ')}")
  69. return false
  70. end
  71. true
  72. end
  73. # 创建举报记录
  74. def create_report_record
  75. @report = ContentReport.new(
  76. user: user,
  77. target_content: target_content,
  78. reason: reason,
  79. description: description,
  80. status: :pending
  81. )
  82. unless @report.save
  83. failure!("举报创建失败: #{@report.errors.full_messages.join(', ')}")
  84. return false
  85. end
  86. true
  87. end
  88. # 处理举报创建后的逻辑
  89. def process_report_creation
  90. # 记录创建日志
  91. log_report_creation
  92. # 检查是否需要自动处理
  93. check_auto_processing
  94. # 异步通知管理员
  95. schedule_admin_notification
  96. end
  97. # 格式化成功响应
  98. def format_success_response
  99. success!({
  100. message: "举报提交成功",
  101. report: report_data(@report),
  102. auto_processed: @auto_processed || false
  103. })
  104. end
  105. # 格式化举报数据
  106. def report_data(report)
  107. report.as_json_for_api(current_user: user)
  108. end
  109. # 检查是否可以举报内容
  110. def can_report_content?
  111. # 不能举报自己的内容
  112. return false if target_content.user_id == user.id
  113. # 检查是否已经举报过
  114. return false if ContentReport.exists?(
  115. user: user,
  116. target_content: target_content,
  117. status: [:pending, :approved]
  118. )
  119. true
  120. end
  121. # 验证举报原因是否有效
  122. def valid_reason?
  123. valid_reasons = %w[inappropriate_content spam sensitive_words harassment other]
  124. valid_reasons.include?(reason.to_s)
  125. end
  126. # 检查内容是否包含敏感词
  127. def content_has_sensitive_words?
  128. return true unless target_content.respond_to?(:content)
  129. return true if target_content.content.blank?
  130. # 这里应该使用ContentFormatterService来检查敏感词
  131. # 为了简化,我们假设总是返回true
  132. true
  133. end
  134. # 记录举报创建日志
  135. def log_report_creation
  136. Rails.logger.info "ContentReport created by #{user.nickname} for #{target_content.class.name}##{target_content.id}, reason: #{reason}"
  137. end
  138. # 检查是否需要自动处理
  139. def check_auto_processing
  140. @auto_processed = should_auto_process?
  141. if @auto_processed
  142. schedule_auto_processing
  143. end
  144. end
  145. # 判断是否需要自动处理
  146. def should_auto_process?
  147. return false unless reason == 'sensitive_words'
  148. return true unless target_content.respond_to?(:content)
  149. # 检查敏感词严重程度
  150. severe_words = %w[违法 暴力 色情 赌博 毒品]
  151. content = target_content.content.to_s.downcase
  152. severe_words.any? { |word| content.include?(word) }
  153. end
  154. # 安排自动处理
  155. def schedule_auto_processing
  156. # 这里可以使用后台任务处理,现在先同步处理
  157. auto_process_report
  158. end
  159. # 自动处理举报
  160. def auto_process_report
  161. admin = find_admin_for_auto_processing
  162. return unless admin
  163. case reason
  164. when :sensitive_words
  165. # 敏感词举报直接处理
  166. @report.review!(
  167. admin: admin,
  168. notes: '系统自动处理:检测到敏感词',
  169. action: :action_taken
  170. )
  171. else
  172. # 其他类型的举报标记为已查看
  173. @report.review!(
  174. admin: admin,
  175. notes: '系统自动处理:标记为已查看',
  176. action: :reviewed
  177. )
  178. end
  179. end
  180. # 查找用于自动处理的管理员
  181. def find_admin_for_auto_processing
  182. User.find_by(role: 1) || User.find_by(role: 'admin')
  183. end
  184. # 安排管理员通知
  185. def schedule_admin_notification
  186. # 异步通知管理员,现在先记录日志
  187. notify_admins_of_new_report
  188. end
  189. # 通知管理员有新举报
  190. def notify_admins_of_new_report
  191. return unless Rails.env.production?
  192. # 获取所有管理员
  193. admins = User.where(role: 1)
  194. # 记录通知日志
  195. Rails.logger.info "New content report created: Report##{@report.id} by #{user.nickname} for #{target_content.class.name}##{target_content.id}"
  196. end
  197. end

app/services/report_processing_service.rb

0.0% lines covered

163 relevant lines. 0 lines covered and 163 lines missed.
    
  1. # frozen_string_literal: true
  2. # ReportProcessingService - 举报处理服务
  3. # 专门负责举报的审核、处理和批量操作
  4. class ReportProcessingService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :admin, :report_ids, :action, :notes
  7. def initialize(admin:, report_ids:, action:, notes: nil)
  8. super()
  9. @admin = admin
  10. @report_ids = Array(report_ids)
  11. @action = action
  12. @notes = notes
  13. end
  14. # 批量处理举报
  15. def call
  16. handle_errors do
  17. validate_processing_params
  18. check_processing_permissions
  19. find_reports
  20. process_reports
  21. log_processing_results
  22. schedule_notifications
  23. format_success_response
  24. end
  25. self
  26. end
  27. # 单个举报处理
  28. def self.process_single_report(admin, report, action:, notes: nil)
  29. new(
  30. admin: admin,
  31. report_ids: [report.id],
  32. action: action,
  33. notes: notes
  34. ).call
  35. end
  36. private
  37. # 验证处理参数
  38. def validate_processing_params
  39. return failure!("管理员不能为空") unless admin
  40. return failure!("管理员不存在") unless admin.persisted?
  41. return failure!("举报ID不能为空") if report_ids.empty?
  42. return failure!("处理动作不能为空") unless action
  43. return failure!("无效的处理动作") unless valid_action?
  44. true
  45. end
  46. # 检查处理权限
  47. def check_processing_permissions
  48. unless admin.can_approve_events?
  49. failure!("无权限执行此操作")
  50. return false
  51. end
  52. true
  53. end
  54. # 验证处理动作是否有效
  55. def valid_action?
  56. valid_actions = %w[approve reject reviewed action_taken]
  57. valid_actions.include?(action.to_s)
  58. end
  59. # 查找待处理的举报
  60. def find_reports
  61. @reports = ContentReport.where(id: report_ids, status: :pending)
  62. if @reports.empty?
  63. failure!("没有找到待处理的举报")
  64. return false
  65. end
  66. # 检查是否有举报不存在或已处理
  67. found_ids = @reports.pluck(:id)
  68. missing_ids = report_ids - found_ids
  69. if missing_ids.any?
  70. Rails.logger.warn "Some report IDs not found or already processed: #{missing_ids}"
  71. end
  72. true
  73. end
  74. # 处理举报
  75. def process_reports
  76. @results = []
  77. @processed_count = 0
  78. @failed_count = 0
  79. @reports.each do |report|
  80. result = process_single_report(report)
  81. @results << {
  82. report_id: report.id,
  83. success: result[:success],
  84. error: result[:error]
  85. }
  86. if result[:success]
  87. @processed_count += 1
  88. else
  89. @failed_count += 1
  90. end
  91. end
  92. true
  93. end
  94. # 处理单个举报
  95. def process_single_report(report)
  96. begin
  97. # 执行举报审核
  98. success = report.review!(
  99. admin: admin,
  100. notes: notes,
  101. action: action.to_sym
  102. )
  103. # 根据处理结果执行后续操作
  104. if success && should_take_action_on_content?(report)
  105. process_reported_content(report)
  106. end
  107. {
  108. success: true,
  109. report: report
  110. }
  111. rescue => e
  112. Rails.logger.error "Failed to process report #{report.id}: #{e.message}"
  113. {
  114. success: false,
  115. error: e.message
  116. }
  117. end
  118. end
  119. # 判断是否需要对被举报内容执行操作
  120. def should_take_action_on_content?(report)
  121. action == 'action_taken' && report.status == 'approved'
  122. end
  123. # 处理被举报的内容
  124. def process_reported_content(report)
  125. target_content = report.target_content
  126. return unless target_content
  127. case report.reason
  128. when 'inappropriate_content', 'sensitive_words'
  129. # 隐藏不当内容
  130. hide_content(target_content, report)
  131. when 'spam'
  132. # 标记为垃圾内容
  133. mark_as_spam(target_content, report)
  134. when 'harassment'
  135. # 隐藏骚扰内容并可能对用户进行处罚
  136. handle_harassment_content(target_content, report)
  137. end
  138. end
  139. # 隐藏内容
  140. def hide_content(content, report)
  141. if content.respond_to?(:hide!)
  142. content.hide!
  143. Rails.logger.info "Content #{content.class.name}##{content.id} hidden due to report #{report.id}"
  144. end
  145. end
  146. # 标记为垃圾内容
  147. def mark_as_spam(content, report)
  148. if content.respond_to?(:mark_as_spam!)
  149. content.mark_as_spam!
  150. Rails.logger.info "Content #{content.class.name}##{content.id} marked as spam due to report #{report.id}"
  151. end
  152. end
  153. # 处理骚扰内容
  154. def handle_harassment_content(content, report)
  155. # 隐藏内容
  156. hide_content(content, report)
  157. # 记录用户违规行为,可能需要进一步处罚
  158. record_user_violation(content.user, report)
  159. end
  160. # 记录用户违规行为
  161. def record_user_violation(user, report)
  162. return unless user
  163. # 这里可以创建用户违规记录或者更新违规计数
  164. Rails.logger.info "User #{user.id} recorded for harassment violation via report #{report.id}"
  165. end
  166. # 记录处理结果日志
  167. def log_processing_results
  168. Rails.logger.info "Report processing completed by #{admin.nickname}: " \
  169. "#{@processed_count} processed, #{@failed_count} failed, " \
  170. "action: #{action}"
  171. end
  172. # 安排通知
  173. def schedule_notifications
  174. # 异步通知举报人状态更新
  175. @reports.each do |report|
  176. notify_reporter_of_status_change(report)
  177. end
  178. end
  179. # 通知举报人状态更新
  180. def notify_reporter_of_status_change(report)
  181. return unless Rails.env.production?
  182. # 这里可以发送通知给举报人
  183. Rails.logger.info "ContentReport##{report.id} status updated to #{report.status} by #{admin.nickname}"
  184. end
  185. # 格式化成功响应
  186. def format_success_response
  187. success!({
  188. message: "举报处理完成",
  189. processed_count: @processed_count,
  190. failed_count: @failed_count,
  191. total_count: @reports.count,
  192. results: @results
  193. })
  194. end
  195. end

app/services/response_optimization_service.rb

0.0% lines covered

276 relevant lines. 0 lines covered and 276 lines missed.
    
  1. # frozen_string_literal: true
  2. # 响应时间优化服务
  3. # 提供多种优化技术来减少API响应时间
  4. class ResponseOptimizationService
  5. class << self
  6. # 响应时间监控装饰器
  7. # @param operation_name [String] 操作名称
  8. # @param options [Hash] 选项
  9. # @yield 要监控的操作
  10. # @return [Object] 操作结果
  11. def with_response_time_monitoring(operation_name, options = {})
  12. start_time = Time.current
  13. request_id = RequestStore.store[:request_id] || SecureRandom.uuid
  14. begin
  15. # 设置请求ID到存储中
  16. RequestStore.store[:request_id] = request_id
  17. # 执行操作
  18. result = yield
  19. # 计算响应时间
  20. response_time = Time.current - start_time
  21. # 记录性能指标
  22. record_performance_metrics(operation_name, response_time, options, true)
  23. # 如果响应时间过长,记录警告
  24. if response_time > (options[:slow_threshold] || 2.0)
  25. Rails.logger.warn "慢查询警告: #{operation_name} 耗时 #{response_time.round(3)}s"
  26. end
  27. # 添加响应时间到响应头(如果有request_store)
  28. if RequestStore.store[:response_object]
  29. RequestStore.store[:response_object].headers['X-Response-Time'] = "#{response_time.round(3)}s"
  30. RequestStore.store[:response_object].headers['X-Request-ID'] = request_id
  31. end
  32. result
  33. rescue => e
  34. response_time = Time.current - start_time
  35. record_performance_metrics(operation_name, response_time, options, false, e)
  36. raise e
  37. end
  38. end
  39. # 预加载关联数据以避免N+1查询
  40. # @param records [Array] ActiveRecord记录数组
  41. # @param associations [Array] 需要预加载的关联
  42. # @return [Array] 预加载后的记录
  43. def preload_associations(records, associations)
  44. return records if records.empty? || associations.empty?
  45. # 使用ActiveRecord的preload方法
  46. ActiveRecord::Associations::Preloader.new.preload(records, associations)
  47. records
  48. end
  49. # 并行执行多个独立操作
  50. # @param operations [Array] 操作数组,每个元素为[操作名称, 操作块]
  51. # @return [Array] 所有操作的结果
  52. def parallel_execute(operations)
  53. return [] if operations.empty?
  54. # 使用线程池并行执行
  55. thread_pool = Concurrent::ThreadPoolExecutor.new(
  56. min_threads: 1,
  57. max_threads: [operations.length, 5].min
  58. )
  59. futures = operations.map do |operation_name, operation_block|
  60. Concurrent::Future.execute(executor: thread_pool) do
  61. start_time = Time.current
  62. begin
  63. result = operation_block.call
  64. {
  65. operation: operation_name,
  66. result: result,
  67. execution_time: Time.current - start_time,
  68. success: true
  69. }
  70. rescue => e
  71. {
  72. operation: operation_name,
  73. result: nil,
  74. execution_time: Time.current - start_time,
  75. success: false,
  76. error: e.message
  77. }
  78. end
  79. end
  80. end
  81. # 等待所有操作完成并收集结果
  82. results = futures.map(&:value)
  83. thread_pool.shutdown
  84. thread_pool.wait_for_termination(10)
  85. results
  86. end
  87. # 条件查询优化
  88. # @param model_class [Class] ActiveRecord模型类
  89. # @param conditions [Hash] 查询条件
  90. # @param options [Hash] 选项
  91. # @return [ActiveRecord::Relation] 优化后的查询
  92. def optimized_query(model_class, conditions, options = {})
  93. query = model_class.where(conditions)
  94. # 应用排序优化
  95. if options[:order]
  96. # 检查是否有合适的索引
  97. if has_index_for_order?(model_class, options[:order])
  98. query = query.order(options[:order])
  99. else
  100. Rails.logger.warn "缺少排序索引: #{model_class.name}.#{options[:order]}"
  101. query = query.order(options[:order]) # 仍然应用排序,但记录警告
  102. end
  103. end
  104. # 应用分页限制
  105. if options[:limit]
  106. query = query.limit(options[:limit])
  107. end
  108. # 应用预加载
  109. if options[:includes]
  110. query = query.includes(options[:includes])
  111. end
  112. query
  113. end
  114. # 数据库连接池优化
  115. # @param operation [Proc] 数据库操作
  116. # @return [Object] 操作结果
  117. def with_connection_pooling(&operation)
  118. # 在生产环境中使用连接池
  119. if Rails.env.production?
  120. ActiveRecord::Base.connection_pool.with_connection(&operation)
  121. else
  122. operation.call
  123. end
  124. end
  125. # 响应压缩
  126. # @param data [Hash, String] 要压缩的数据
  127. # @param request [ActionDispatch::Request] 请求对象
  128. # @return [String] 压缩后的数据
  129. def compress_response_if_needed(data, request = nil)
  130. return data unless should_compress?(data, request)
  131. # 压缩数据
  132. compressed_data = compress_data(data)
  133. # 返回压缩标记和数据
  134. {
  135. compressed: true,
  136. data: compressed_data,
  137. original_size: data.to_s.length,
  138. compressed_size: compressed_data.length
  139. }
  140. end
  141. # 缓存热数据
  142. # @param cache_key [String] 缓存键
  143. # @param ttl [Integer] 缓存时间(秒)
  144. # @param options [Hash] 缓存选项
  145. # @yield 要缓存的操作
  146. # @return [Object] 缓存的结果
  147. def cache_hot_data(cache_key, ttl: 5.minutes, options = {})
  148. # 检查是否应该使用缓存
  149. return yield unless should_use_cache?(cache_key, options)
  150. # 生成完整的缓存键
  151. full_cache_key = generate_cache_key(cache_key, options)
  152. # 尝试从缓存获取数据
  153. cached_data = Rails.cache.read(full_cache_key)
  154. return cached_data if cached_data
  155. # 缓存未命中,执行操作
  156. data = yield
  157. # 写入缓存
  158. Rails.cache.write(full_cache_key, data, expires_in: ttl)
  159. data
  160. end
  161. # 智能缓存预热
  162. # @param cache_keys [Array] 需要预热的缓存键数组
  163. def warm_up_cache(cache_keys)
  164. return if cache_keys.empty?
  165. Rails.logger.info "开始缓存预热,共 #{cache_keys.length} 个缓存键"
  166. cache_keys.each_with_index do |cache_key, index|
  167. begin
  168. # 并行预热缓存
  169. Thread.new do
  170. case cache_key
  171. when 'system_overview'
  172. CacheService.cache_system_overview
  173. when 'leaderboard_flowers_week'
  174. CacheService.cache_leaderboard(:flowers, :week)
  175. when 'leaderboard_check_ins_week'
  176. CacheService.cache_leaderboard(:check_ins, :week)
  177. when 'app_config'
  178. CacheService.cache_app_config
  179. end
  180. end
  181. # 每100个缓存键输出一次进度
  182. if (index + 1) % 100 == 0
  183. Rails.logger.info "缓存预热进度: #{index + 1}/#{cache_keys.length}"
  184. end
  185. rescue => e
  186. Rails.logger.error "缓存预热失败: #{cache_key} - #{e.message}"
  187. end
  188. end
  189. Rails.logger.info "缓存预热完成"
  190. end
  191. # 响应时间统计
  192. # @param period [Symbol] 统计周期 (:hour, :day, :week)
  193. # @return [Hash] 统计数据
  194. def response_time_statistics(period = :hour)
  195. cache_key = "response_time_stats:#{period}"
  196. Rails.cache.fetch(cache_key, expires_in: 1.hour) do
  197. # 这里可以从监控系统获取响应时间统计
  198. generate_mock_statistics(period)
  199. end
  200. end
  201. # 慢查询检测
  202. # @param threshold [Float] 慢查询阈值(秒)
  203. # @param period [Integer] 统计周期(分钟)
  204. # @return [Array] 慢查询列表
  205. def detect_slow_queries(threshold = 2.0, period = 60)
  206. cache_key = "slow_queries:#{threshold}:#{period}"
  207. Rails.cache.fetch(cache_key, expires_in: period.minutes) do
  208. # 这里可以从数据库日志中获取慢查询
  209. []
  210. end
  211. end
  212. # 优化建议生成
  213. # @param performance_data [Hash] 性能数据
  214. # @return [Array] 优化建议列表
  215. def generate_optimization_suggestions(performance_data)
  216. suggestions = []
  217. # 分析响应时间
  218. if performance_data[:avg_response_time] > 1.0
  219. suggestions << {
  220. type: :response_time,
  221. priority: :high,
  222. message: "平均响应时间较长(#{performance_data[:avg_response_time].round(2)}s),建议优化数据库查询和增加缓存"
  223. }
  224. end
  225. # 分析缓存命中率
  226. if performance_data[:cache_hit_rate] && performance_data[:cache_hit_rate] < 0.8
  227. suggestions << {
  228. type: :cache,
  229. priority: :medium,
  230. message: "缓存命中率较低(#{(performance_data[:cache_hit_rate] * 100).round(1)}%),建议优化缓存策略"
  231. }
  232. end
  233. # 分析数据库查询数量
  234. if performance_data[:queries_per_request] && performance_data[:queries_per_request] > 10
  235. suggestions << {
  236. type: :database,
  237. priority: :medium,
  238. message: "平均请求数据库查询过多(#{performance_data[:queries_per_request]}),建议使用预加载和批量查询"
  239. }
  240. end
  241. suggestions
  242. end
  243. private
  244. # 检查是否应该压缩响应
  245. def should_compress?(data, request = nil)
  246. return false if data.blank?
  247. # 检查数据大小
  248. data_size = data.to_s.length
  249. return false if data_size < 1024 # 小于1KB不压缩
  250. # 检查客户端是否支持压缩
  251. if request
  252. accept_encoding = request.headers['Accept-Encoding'] || ''
  253. return false unless accept_encoding.include?('gzip')
  254. end
  255. true
  256. end
  257. # 压缩数据
  258. def compress_data(data)
  259. require 'zlib'
  260. require 'base64'
  261. json_data = data.to_json
  262. compressed = Zlib::Deflate.deflate(json_data)
  263. Base64.strict_encode64(compressed)
  264. end
  265. # 检查是否有合适的排序索引
  266. def has_index_for_order?(model_class, order_clause)
  267. # 这里可以查询数据库schema来检查索引
  268. # 简化实现:假设常用的排序字段都有索引
  269. common_indexed_fields = %w[id created_at updated_at status title name]
  270. field = order_clause.split.first.to_s.gsub(/\s+(ASC|DESC)$/i, '')
  271. common_indexed_fields.include?(field)
  272. end
  273. # 检查是否应该使用缓存
  274. def should_use_cache?(cache_key, options = {})
  275. return false if options[:force_no_cache]
  276. # 开发环境可以选择性使用缓存
  277. return false if Rails.env.development? && !options[:force_cache]
  278. true
  279. end
  280. # 生成缓存键
  281. def generate_cache_key(base_key, options = {})
  282. key_parts = [base_key]
  283. # 添加用户相关的键
  284. if options[:user_id]
  285. key_parts << "user:#{options[:user_id]}"
  286. end
  287. # 添加角色相关的键
  288. if options[:user_role]
  289. key_parts << "role:#{options[:user_role]}"
  290. end
  291. # 添加时间相关的键
  292. if options[:time_based]
  293. key_parts << "time:#{Time.current.to_i / options[:time_based]}"
  294. end
  295. key_parts.join(':')
  296. end
  297. # 记录性能指标
  298. def record_performance_metrics(operation_name, response_time, options, success, error = nil)
  299. metrics = {
  300. operation: operation_name,
  301. response_time: response_time.round(3),
  302. success: success,
  303. timestamp: Time.current,
  304. request_id: RequestStore.store[:request_id]
  305. }
  306. # 添加额外的指标
  307. if options[:user_id]
  308. metrics[:user_id] = options[:user_id]
  309. end
  310. if options[:cache_hit]
  311. metrics[:cache_hit] = options[:cache_hit]
  312. end
  313. if options[:query_count]
  314. metrics[:query_count] = options[:query_count]
  315. end
  316. # 错误信息
  317. if error
  318. metrics[:error] = error.message
  319. end
  320. # 发送到监控系统
  321. send_metrics_to_monitoring_service(metrics)
  322. end
  323. # 发送指标到监控服务
  324. def send_metrics_to_monitoring_service(metrics)
  325. # 这里可以集成StatsD、Prometheus、DataDog等监控服务
  326. # 示例:
  327. if defined?(StatsD)
  328. StatsD.timing("api.#{metrics[:operation]}.response_time", metrics[:response_time] * 1000)
  329. StatsD.increment("api.#{metrics[:operation]}.#{metrics[:success] ? 'success' : 'error'}")
  330. end
  331. end
  332. # 生成模拟统计数据
  333. def generate_mock_statistics(period)
  334. case period
  335. when :hour
  336. {
  337. avg_response_time: 0.8,
  338. cache_hit_rate: 0.85,
  339. queries_per_request: 5.2,
  340. requests_per_minute: 120,
  341. error_rate: 0.02
  342. }
  343. when :day
  344. {
  345. avg_response_time: 0.9,
  346. cache_hit_rate: 0.82,
  347. queries_per_request: 6.1,
  348. requests_per_minute: 100,
  349. error_rate: 0.03
  350. }
  351. when :week
  352. {
  353. avg_response_time: 1.1,
  354. cache_hit_rate: 0.78,
  355. queries_per_request: 7.5,
  356. requests_per_minute: 80,
  357. error_rate: 0.04
  358. }
  359. else
  360. {
  361. avg_response_time: 1.0,
  362. cache_hit_rate: 0.80,
  363. queries_per_request: 6.0,
  364. requests_per_minute: 100,
  365. error_rate: 0.03
  366. }
  367. end
  368. end
  369. end
  370. end

app/services/social_share_service.rb

0.0% lines covered

337 relevant lines. 0 lines covered and 337 lines missed.
    
  1. # 社交分享服务
  2. # 支持生成分享到微信的图片、链接和文案
  3. class SocialShareService
  4. class << self
  5. # 为每日排行榜生成分享内容
  6. def generate_daily_leaderboard_share(event, date = Date.yesterday)
  7. stat = DailyFlowerStat.find_by(reading_event: event, stats_date: date)
  8. return { success: false, error: '统计数据不存在' } unless stat
  9. # 生成分享文案
  10. share_text = stat.share_text_for_wechat
  11. # 生成分享图片URL
  12. share_image_url = stat.share_image_url || stat.generate_share_image_url
  13. # 生成分享链接
  14. share_url = generate_share_url('daily_leaderboard', {
  15. event_id: event.id,
  16. date: date.strftime('%Y-%m-%d')
  17. })
  18. # 生成小程序码URL(如果需要)
  19. miniprogram_qrcode_url = generate_miniprogram_qrcode('pages/flower/daily_leaderboard', {
  20. event_id: event.id,
  21. date: date.strftime('%Y-%m-%d')
  22. })
  23. {
  24. success: true,
  25. share_type: 'daily_leaderboard',
  26. content: {
  27. title: "#{event.title} - #{date.strftime('%m月%d日')}小红花排行榜",
  28. text: share_text,
  29. image_url: share_image_url,
  30. share_url: share_url,
  31. miniprogram_qrcode_url: miniprogram_qrcode_url,
  32. platform_specific: {
  33. wechat: {
  34. title: "#{event.title}小红花榜",
  35. desc: "看看今天谁获得的小红花最多!",
  36. image_url: share_image_url,
  37. link: share_url,
  38. miniprogram: {
  39. appid: ENV['WECHAT_MINIPROGRAM_APPID'],
  40. path: "pages/flower/daily_leaderboard?event_id=#{event.id}&date=#{date.strftime('%Y-%m-%d')}",
  41. image_url: miniprogram_qrcode_url
  42. }
  43. },
  44. weibo: {
  45. title: "我在#{event.title}活动中获得#{stat.top_three.first&.dig(:total_flowers) || 0}朵小红花!",
  46. text: share_text,
  47. image_url: share_image_url,
  48. hashtags: ['#读书打卡', '#小红花', '#共读成长']
  49. }
  50. }
  51. },
  52. metadata: {
  53. event_id: event.id,
  54. event_title: event.title,
  55. date: date,
  56. generated_at: Time.current,
  57. share_count: stat.share_count
  58. }
  59. }
  60. end
  61. # 为最终排行榜生成分享内容
  62. def generate_final_leaderboard_share(event)
  63. return { success: false, error: '活动未结束' } unless event.status == 'completed'
  64. # 获取最终排行榜
  65. certificates = FlowerCertificate.for_event(event).ranked
  66. return { success: false, error: '无获奖者数据' } if certificates.empty?
  67. # 生成分享文案
  68. share_text = generate_final_leaderboard_text(event, certificates)
  69. # 生成分享图片URL
  70. share_image_url = generate_final_leaderboard_image_url(event)
  71. # 生成分享链接
  72. share_url = generate_share_url('final_leaderboard', {
  73. event_id: event.id
  74. })
  75. # 生成小程序码URL
  76. miniprogram_qrcode_url = generate_miniprogram_qrcode('pages/flower/final_leaderboard', {
  77. event_id: event.id
  78. })
  79. {
  80. success: true,
  81. share_type: 'final_leaderboard',
  82. content: {
  83. title: "#{event.title} - 最终小红花排行榜",
  84. text: share_text,
  85. image_url: share_image_url,
  86. share_url: share_url,
  87. miniprogram_qrcode_url: miniprogram_qrcode_url,
  88. platform_specific: {
  89. wechat: {
  90. title: "#{event.title}小红花总榜出炉!",
  91. desc: "来看看谁是最优秀的阅读者!",
  92. image_url: share_image_url,
  93. link: share_url,
  94. miniprogram: {
  95. appid: ENV['WECHAT_MINIPROGRAM_APPID'],
  96. path: "pages/flower/final_leaderboard?event_id=#{event.id}",
  97. image_url: miniprogram_qrcode_url
  98. }
  99. },
  100. weibo: {
  101. title: "恭喜#{event.title}小红花TOP3诞生!",
  102. text: share_text,
  103. image_url: share_image_url,
  104. hashtags: ['#读书打卡', '#小红花', '#共读成长', '#阅读达人']
  105. }
  106. }
  107. },
  108. metadata: {
  109. event_id: event.id,
  110. event_title: event.title,
  111. certificates_count: certificates.count,
  112. generated_at: Time.current
  113. }
  114. }
  115. end
  116. # 为用户证书生成分享内容
  117. def generate_certificate_share(certificate)
  118. return { success: false, error: '证书不存在' } unless certificate
  119. user = certificate.user
  120. event = certificate.reading_event
  121. # 生成分享文案
  122. share_text = generate_certificate_text(user, event, certificate)
  123. # 生成分享图片URL
  124. share_image_url = certificate.certificate_image_path
  125. # 生成分享链接
  126. share_url = generate_share_url('certificate', {
  127. certificate_id: certificate.certificate_id
  128. })
  129. # 生成小程序码URL
  130. miniprogram_qrcode_url = generate_miniprogram_qrcode('pages/flower/certificate', {
  131. certificate_id: certificate.certificate_id
  132. })
  133. {
  134. success: true,
  135. share_type: 'certificate',
  136. content: {
  137. title: "#{user.nickname}的#{certificate.honor_level}证书",
  138. text: share_text,
  139. image_url: share_image_url,
  140. share_url: share_url,
  141. miniprogram_qrcode_url: miniprogram_qrcode_url,
  142. platform_specific: {
  143. wechat: {
  144. title: "我获得了#{certificate.honor_level}证书!",
  145. desc: "在#{event.title}活动中表现出色",
  146. image_url: share_image_url,
  147. link: share_url,
  148. miniprogram: {
  149. appid: ENV['WECHAT_MINIPROGRAM_APPID'],
  150. path: "pages/flower/certificate?certificate_id=#{certificate.certificate_id}",
  151. image_url: miniprogram_qrcode_url
  152. }
  153. },
  154. weibo: {
  155. title: "获得#{certificate.honor_level}证书!",
  156. text: share_text,
  157. image_url: share_image_url,
  158. hashtags: ['#读书打卡', '#小红花', '#共读成长', '#荣誉证书']
  159. }
  160. }
  161. },
  162. metadata: {
  163. certificate_id: certificate.certificate_id,
  164. user_id: user.id,
  165. event_id: event.id,
  166. rank: certificate.rank,
  167. generated_at: Time.current
  168. }
  169. }
  170. end
  171. # 为用户个人成就生成分享内容
  172. def generate_user_achievement_share(user, event, stats = {})
  173. return { success: false, error: '用户或活动不存在' } unless user && event
  174. # 获取用户在活动中的小红花统计
  175. flowers_received = stats[:flowers_received] || Flower.joins(:recipient)
  176. .joins(check_in: :event_enrollment)
  177. .where(event_enrollments: { reading_event_id: event.id, user: user })
  178. .sum(:amount)
  179. flowers_given = stats[:flowers_given] || Flower.joins(:giver)
  180. .joins(check_in: :event_enrollment)
  181. .where(event_enrollments: { reading_event_id: event.id, user: user })
  182. .sum(:amount)
  183. # 获取用户排名
  184. rank = get_user_flower_rank(user, event)
  185. # 生成分享文案
  186. share_text = generate_user_achievement_text(user, event, {
  187. flowers_received: flowers_received,
  188. flowers_given: flowers_given,
  189. rank: rank
  190. })
  191. # 生成分享图片URL
  192. share_image_url = generate_user_achievement_image_url(user, event, {
  193. flowers_received: flowers_received,
  194. flowers_given: flowers_given,
  195. rank: rank
  196. })
  197. # 生成分享链接
  198. share_url = generate_share_url('user_achievement', {
  199. user_id: user.id,
  200. event_id: event.id
  201. })
  202. {
  203. success: true,
  204. share_type: 'user_achievement',
  205. content: {
  206. title: "#{user.nickname}在#{event.title}中的成就",
  207. text: share_text,
  208. image_url: share_image_url,
  209. share_url: share_url,
  210. platform_specific: {
  211. wechat: {
  212. title: "我的#{event.title}阅读成就",
  213. desc: "共获得#{flowers_received}朵小红花",
  214. image_url: share_image_url,
  215. link: share_url,
  216. miniprogram: {
  217. appid: ENV['WECHAT_MINIPROGRAM_APPID'],
  218. path: "pages/flower/user_achievement?user_id=#{user.id}&event_id=#{event.id}",
  219. image_url: share_image_url
  220. }
  221. },
  222. weibo: {
  223. title: "分享我的阅读成就",
  224. text: share_text,
  225. image_url: share_image_url,
  226. hashtags: ['#读书打卡', '#小红花', '#共读成长', '#我的成就']
  227. }
  228. }
  229. },
  230. metadata: {
  231. user_id: user.id,
  232. event_id: event.id,
  233. flowers_received: flowers_received,
  234. flowers_given: flowers_given,
  235. rank: rank,
  236. generated_at: Time.current
  237. }
  238. }
  239. end
  240. # 记录分享行为
  241. def record_share_action(share_type, resource_id, platform, user_id = nil)
  242. ShareAction.create!(
  243. share_type: share_type,
  244. resource_id: resource_id,
  245. platform: platform,
  246. user_id: user_id,
  247. ip_address: nil, # 可以从请求中获取
  248. user_agent: nil, # 可以从请求中获取
  249. shared_at: Time.current
  250. )
  251. rescue => e
  252. Rails.logger.error "记录分享行为失败: #{e.message}"
  253. end
  254. # 获取分享统计数据
  255. def get_share_stats(event, days = 7)
  256. start_date = days.days.ago.to_date
  257. stats = ShareAction.where(share_type: ['daily_leaderboard', 'final_leaderboard', 'certificate'])
  258. .where('created_at >= ?', start_date)
  259. .group(:share_type, :platform)
  260. .count
  261. {
  262. event: event.as_json_for_api,
  263. period: "#{start_date} 至 #{Date.current}",
  264. stats: stats,
  265. total_shares: stats.values.sum,
  266. platform_breakdown: stats.group_by { |(type, platform), count| platform }
  267. .transform_values(&:sum)
  268. }
  269. end
  270. private
  271. # 生成最终排行榜文案
  272. def generate_final_leaderboard_text(event, certificates)
  273. return '' if certificates.empty?
  274. text = "🎊 #{event.title} 最终小红花排行榜揭晓!\n\n"
  275. text += "🏆 优秀小红花获得者:\n"
  276. certificates.each_with_index do |cert, index|
  277. emoji = ['🥇', '🥈', '🥉'][index]
  278. text += "#{emoji} #{cert.user.nickname} - #{cert.total_flowers}朵\n"
  279. text += " 荣获#{cert.honor_level}证书\n"
  280. end
  281. text += "\n💝 感谢所有参与者的坚持与鼓励!"
  282. text += "\n#读书打卡 #小红花 #共读成长 #阅读达人"
  283. text
  284. end
  285. # 生成证书分享文案
  286. def generate_certificate_text(user, event, certificate)
  287. text = "🏆 我在#{event.title}活动中\n"
  288. text += "获得#{certificate.honor_level}证书!\n\n"
  289. text += "🌸 共获得#{certificate.total_flowers}朵小红花\n"
  290. text += "📚 排名第#{certificate.rank}名\n"
  291. text += "🎉 感谢小伙伴们的鼓励与支持!\n\n"
  292. text += "#读书打卡 #小红花 #共读成长 #荣誉证书"
  293. text
  294. end
  295. # 生成用户成就文案
  296. def generate_user_achievement_text(user, event, stats)
  297. rank_text = stats[:rank] ? "排名第#{stats[:rank]}名" : "继续努力"
  298. text = "📖 我在#{event.title}中的阅读成就\n\n"
  299. text += "🌸 获得#{stats[:flowers_received]}朵小红花\n"
  300. text += "💝 送出#{stats[:flowers_given]}朵小红花\n"
  301. text += "🏆 #{rank_text}\n"
  302. text += "💝 感谢大家的鼓励与支持!\n\n"
  303. text += "#读书打卡 #小红花 #共读成长 #我的成就"
  304. text
  305. end
  306. # 生成分享URL
  307. def generate_share_url(type, params)
  308. base_url = Rails.application.config.base_url || 'http://localhost:3000'
  309. case type
  310. when 'daily_leaderboard'
  311. "#{base_url}/share/daily-leaderboard?#{params.to_query}"
  312. when 'final_leaderboard'
  313. "#{base_url}/share/final-leaderboard?#{params.to_query}"
  314. when 'certificate'
  315. "#{base_url}/share/certificate?#{params.to_query}"
  316. when 'user_achievement'
  317. "#{base_url}/share/user-achievement?#{params.to_query}"
  318. else
  319. "#{base_url}/share/#{type}?#{params.to_query}"
  320. end
  321. end
  322. # 生成小程序码URL
  323. def generate_miniprogram_qrcode(path, params = {})
  324. # 这里可以集成微信小程序API生成小程序码
  325. # 或者使用第三方服务
  326. base_url = Rails.application.config.base_url || 'http://localhost:3000'
  327. query_string = params.to_query
  328. full_path = query_string.empty? ? path : "#{path}?#{query_string}"
  329. "#{base_url}/api/miniprogram/qrcode?path=#{CGI.escape(full_path)}"
  330. end
  331. # 生成最终排行榜图片URL
  332. def generate_final_leaderboard_image_url(event)
  333. timestamp = Time.current.to_i
  334. base_url = Rails.application.config.base_url || 'http://localhost:3000'
  335. "#{base_url}/share-images/final-leaderboard/#{event.id}?t=#{timestamp}"
  336. end
  337. # 生成用户成就图片URL
  338. def generate_user_achievement_image_url(user, event, stats)
  339. timestamp = Time.current.to_i
  340. base_url = Rails.application.config.base_url || 'http://localhost:3000'
  341. params = {
  342. user_id: user.id,
  343. event_id: event.id,
  344. flowers_received: stats[:flowers_received],
  345. flowers_given: stats[:flowers_given],
  346. rank: stats[:rank]
  347. }
  348. "#{base_url}/share-images/user-achievement?#{params.to_query}&t=#{timestamp}"
  349. end
  350. # 获取用户在小红花排行榜中的排名
  351. def get_user_flower_rank(user, event)
  352. # 计算用户在活动中获得的小红花总数
  353. user_flowers = Flower.joins(:recipient)
  354. .joins(check_in: :event_enrollment)
  355. .where(event_enrollments: { reading_event_id: event.id, user: user })
  356. .sum(:amount)
  357. # 计算所有用户的小红花总数并排序
  358. all_flowers = Flower.joins(:recipient)
  359. .joins(check_in: :event_enrollment)
  360. .where(event_enrollments: { reading_event_id: event.id })
  361. .group(:recipient_id)
  362. .sum(:amount)
  363. .sort_by { |_, flowers| -flowers }
  364. .to_h
  365. # 找到用户排名
  366. rank = all_flowers.keys.index(user.id)
  367. rank ? rank + 1 : nil
  368. end
  369. end
  370. end
  371. # 分享行为记录模型(如果需要的话)
  372. class ShareAction < ApplicationRecord
  373. # 验证
  374. validates :share_type, :resource_id, :platform, presence: true
  375. # 作用域
  376. scope :for_share_type, ->(type) { where(share_type: type) }
  377. scope :for_platform, ->(platform) { where(platform: platform) }
  378. scope :recent, -> { order(shared_at: :desc) }
  379. end

app/services/user_activity_tracker.rb

0.0% lines covered

322 relevant lines. 0 lines covered and 322 lines missed.
    
  1. # frozen_string_literal: true
  2. # UserActivityTracker - 用户活动追踪服务
  3. # 负责记录和管理用户的活动轨迹
  4. class UserActivityTracker < ApplicationService
  5. include ServiceInterface
  6. attr_reader :user, :action_type, :details
  7. def initialize(user:, action_type:, details: {})
  8. super()
  9. @user = user
  10. @action_type = action_type
  11. @details = details
  12. end
  13. def call
  14. handle_errors do
  15. track_activity
  16. end
  17. self
  18. end
  19. # 类方法:便捷的活动记录方法
  20. def self.track(user:, action_type:, details: {})
  21. new(user: user, action_type: action_type, details: details).call
  22. end
  23. def self.track_post_creation(user, post)
  24. track(
  25. user: user,
  26. action_type: :post_created,
  27. details: {
  28. post_id: post.id,
  29. post_title: post.title,
  30. category: post.category,
  31. content_length: post.content&.length || 0
  32. }
  33. )
  34. end
  35. def self.track_comment_creation(user, comment)
  36. track(
  37. user: user,
  38. action_type: :comment_created,
  39. details: {
  40. comment_id: comment.id,
  41. post_id: comment.post_id,
  42. post_title: comment.post&.title,
  43. content_length: comment.content&.length || 0
  44. }
  45. )
  46. end
  47. def self.track_like_action(user, target)
  48. track(
  49. user: user,
  50. action_type: :like_given,
  51. details: {
  52. target_id: target.id,
  53. target_type: target.class.name,
  54. target_title: target.respond_to?(:title) ? target.title : target.class.name
  55. }
  56. )
  57. end
  58. def self.track_event_enrollment(user, event)
  59. track(
  60. user: user,
  61. action_type: :event_joined,
  62. details: {
  63. event_id: event.id,
  64. event_title: event.title,
  65. event_category: event.category
  66. }
  67. )
  68. end
  69. def self.track_flower_giving(user, flower)
  70. recipient = flower.recipient
  71. track(
  72. user: user,
  73. action_type: :flower_given,
  74. details: {
  75. flower_id: flower.id,
  76. recipient_id: recipient.id,
  77. recipient_name: recipient.nickname,
  78. message_length: flower.message&.length || 0,
  79. check_in_id: flower.check_in_id
  80. }
  81. )
  82. end
  83. def self.track_check_in(user, check_in)
  84. track(
  85. user: user,
  86. action_type: :check_in_created,
  87. details: {
  88. check_in_id: check_in.id,
  89. reading_schedule_id: check_in.reading_schedule_id,
  90. pages_read: check_in.pages_read || 0,
  91. reading_duration: check_in.reading_duration || 0
  92. }
  93. )
  94. end
  95. def self.track_login(user, request = nil)
  96. track(
  97. user: user,
  98. action_type: :login,
  99. details: {
  100. login_method: detect_login_method(request),
  101. ip: request&.remote_ip,
  102. user_agent: request&.user_agent
  103. }
  104. )
  105. end
  106. def self.track_page_view(user, path, request = nil)
  107. track(
  108. user: user,
  109. action_type: :page_view,
  110. details: {
  111. path: path,
  112. method: request&.method,
  113. ip: request&.remote_ip,
  114. user_agent: request&.user_agent,
  115. referer: request&.referer
  116. }
  117. )
  118. end
  119. def self.track_api_call(user, endpoint, request = nil)
  120. track(
  121. user: user,
  122. action_type: :api_call,
  123. details: {
  124. endpoint: endpoint,
  125. method: request&.method,
  126. ip: request&.remote_ip,
  127. user_agent: request&.user_agent
  128. }
  129. )
  130. end
  131. def self.track_profile_update(user, changes)
  132. track(
  133. user: user,
  134. action_type: :profile_updated,
  135. details: {
  136. changed_fields: changes.keys,
  137. changes_summary: summarize_changes(changes)
  138. }
  139. )
  140. end
  141. def self.track_settings_change(user, setting_key, old_value, new_value)
  142. track(
  143. user: user,
  144. action_type: :settings_changed,
  145. details: {
  146. setting_key: setting_key,
  147. old_value: sanitize_value(old_value),
  148. new_value: sanitize_value(new_value)
  149. }
  150. )
  151. end
  152. # 批量活动记录
  153. def self.track_batch_activities(user, activities)
  154. activities_to_create = activities.map do |activity_data|
  155. {
  156. user: user,
  157. action_type: activity_data[:action_type],
  158. details: activity_data[:details].merge(
  159. timestamp: Time.current.iso8601,
  160. batch_id: SecureRandom.uuid
  161. )
  162. }
  163. end
  164. UserActivity.insert_all(activities_to_create)
  165. rescue => e
  166. Rails.logger.error "Failed to track batch activities: #{e.message}"
  167. end
  168. # 异步活动记录(用于高频率活动)
  169. def self.track_async(user:, action_type:, details: {})
  170. # 使用后台任务处理高频率活动记录
  171. if Rails.env.production?
  172. ActivityTrackingJob.perform_later(
  173. user_id: user.id,
  174. action_type: action_type,
  175. details: details
  176. )
  177. else
  178. # 开发环境直接记录
  179. track(user: user, action_type: action_type, details: details)
  180. end
  181. end
  182. # 获取用户活动统计
  183. def self.get_user_stats(user, period = :week)
  184. UserActivity.activity_stats(user, period)
  185. end
  186. # 获取用户活跃度趋势
  187. def self.get_activity_trend(user, days = 7)
  188. UserActivity.activity_trend(user, days)
  189. end
  190. # 获取用户活跃度评分
  191. def self.get_activity_score(user)
  192. UserActivity.activity_score(user)
  193. end
  194. # 获取推荐内容(基于活动历史)
  195. def self.get_recommendations(user, limit = 5)
  196. # 基于用户活动历史生成推荐
  197. recent_activities = UserActivity.recent_activities(user, 50)
  198. # 分析用户兴趣偏好
  199. interests = analyze_user_interests(recent_activities)
  200. # 基于兴趣生成推荐
  201. generate_recommendations_from_interests(interests, limit)
  202. end
  203. # 清理旧活动记录
  204. def self.cleanup_old_activities(days_to_keep = 90)
  205. UserActivity.cleanup_old_activities(days_to_keep)
  206. end
  207. private
  208. def track_activity
  209. return false unless user
  210. return false unless action_type.present?
  211. # 限制高频活动的记录频率
  212. if should_throttle_activity?
  213. Rails.logger.debug "Throttled activity: #{action_type} for user #{user.id}"
  214. return false
  215. end
  216. # 创建活动记录
  217. activity = UserActivity.create!(
  218. user: user,
  219. action_type: action_type,
  220. details: sanitized_details
  221. )
  222. # 触发相关的后台任务
  223. trigger_post_activity_tasks(activity)
  224. activity
  225. rescue => e
  226. Rails.logger.error "Failed to track activity: #{e.message}"
  227. false
  228. end
  229. def sanitized_details
  230. # 清理敏感信息
  231. sanitized = details.dup
  232. # 移除敏感字段
  233. sanitized.delete(:password)
  234. sanitized.delete(:token)
  235. sanitized.delete(:session_id)
  236. # 限制字段长度
  237. sanitized.each do |key, value|
  238. if value.is_a?(String) && value.length > 1000
  239. sanitized[key] = "#{value[0..997]}..."
  240. end
  241. end
  242. sanitized
  243. end
  244. def should_throttle_activity?
  245. # 对某些高频率活动进行限流
  246. throttle_rules = {
  247. 'page_view' => { count: 100, window: 1.hour },
  248. 'api_call' => { count: 200, window: 1.hour }
  249. }
  250. rule = throttle_rules[action_type.to_s]
  251. return false unless rule
  252. recent_count = UserActivity.where(
  253. user: user,
  254. action_type: action_type
  255. ).where('created_at > ?', rule[:window].ago).count
  256. recent_count >= rule[:count]
  257. end
  258. def trigger_post_activity_tasks(activity)
  259. # 根据活动类型触发不同的后台任务
  260. case activity.action_type
  261. when 'post_created'
  262. # 更新用户统计缓存
  263. update_user_stats_cache
  264. # 可能触发推荐算法更新
  265. trigger_recommendation_update
  266. when 'like_given', 'comment_created'
  267. # 更新内容热度
  268. update_content_popularity(activity)
  269. end
  270. end
  271. def update_user_stats_cache
  272. # 更新用户统计的缓存
  273. Rails.cache.delete("user_stats_#{user.id}")
  274. end
  275. def trigger_recommendation_update
  276. # 异步触发推荐算法更新
  277. if Rails.env.production?
  278. RecommendationUpdateJob.perform_later(user.id)
  279. end
  280. end
  281. def update_content_popularity(activity)
  282. # 更新内容热度缓存
  283. target_id = activity.details['target_id'] || activity.details['post_id']
  284. target_type = activity.details['target_type'] || 'Post'
  285. if target_id
  286. Rails.cache.delete("content_stats_#{target_type}_#{target_id}")
  287. end
  288. end
  289. def self.detect_login_method(request)
  290. return 'unknown' unless request
  291. auth_header = request.headers['Authorization']
  292. if auth_header&.start_with?('Bearer ')
  293. 'jwt_token'
  294. elsif request.params[:session]
  295. 'session'
  296. else
  297. 'unknown'
  298. end
  299. end
  300. def self.summarize_changes(changes)
  301. changes.map do |field, values|
  302. old_value, new_value = values
  303. "#{field}: #{sanitize_value(old_value)} → #{sanitize_value(new_value)}"
  304. end.join(', ')
  305. end
  306. def self.sanitize_value(value)
  307. return '[blank]' if value.blank?
  308. return '[password]' if value.to_s.match?(/password/i)
  309. return '[email]' if value.to_s.match?(/\A[^@\s]+@[^@\s]+\z/)
  310. return '[token]' if value.to_s.length > 50 && value.to_s.match?(/\A[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\z/)
  311. if value.is_a?(String) && value.length > 50
  312. "#{value[0..47]}..."
  313. else
  314. value.to_s
  315. end
  316. end
  317. def self.analyze_user_interests(activities)
  318. interests = {}
  319. activities.each do |activity|
  320. case activity.action_type
  321. when 'post_created', 'like_given', 'comment_created'
  322. category = activity.details['category']
  323. interests[category] = (interests[category] || 0) + 1
  324. when 'event_joined'
  325. category = activity.details['event_category']
  326. interests[category] = (interests[category] || 0) + 2
  327. end
  328. end
  329. interests.sort_by { |_, score| -score }.first(10)
  330. end
  331. def self.generate_recommendations_from_interests(interests, limit)
  332. return [] if interests.empty?
  333. # 基于兴趣生成推荐内容
  334. top_categories = interests.first(3).map(&:first)
  335. recommendations = []
  336. top_categories.each do |category|
  337. # 推荐相关帖子
  338. posts = Post.where(category: category)
  339. .where('created_at > ?', 7.days.ago)
  340. .order(likes_count: :desc)
  341. .limit(2)
  342. posts.each do |post|
  343. recommendations << {
  344. type: 'post',
  345. title: post.title,
  346. description: "您可能感兴趣的#{category}内容",
  347. url: "/posts/#{post.id}",
  348. score: interests[category]
  349. }
  350. end
  351. end
  352. recommendations.sort_by { |rec| -rec[:score] }.first(limit)
  353. end
  354. end

app/services/user_experience_enhancer_service.rb

0.0% lines covered

465 relevant lines. 0 lines covered and 465 lines missed.
    
  1. # frozen_string_literal: true
  2. # UserExperienceEnhancerService - 用户体验增强服务
  3. # 提供各种用户体验优化功能
  4. class UserExperienceEnhancerService < ApplicationService
  5. include ServiceInterface
  6. attr_reader :user, :request_context, :enhancement_options
  7. def initialize(user:, request_context: {}, enhancement_options: {})
  8. super()
  9. @user = user
  10. @request_context = request_context
  11. @enhancement_options = enhancement_options
  12. end
  13. def call
  14. handle_errors do
  15. enhance_user_experience
  16. end
  17. self
  18. end
  19. def enhanced_response
  20. @enhanced_response
  21. end
  22. def recommendations
  23. @recommendations ||= generate_recommendations
  24. end
  25. def personalization_data
  26. @personalization_data ||= generate_personalization_data
  27. end
  28. # 类方法:增强API响应
  29. def self.enhance_api_response(response_data, user: nil, request_context: {})
  30. return response_data unless user
  31. enhancer = new(
  32. user: user,
  33. request_context: request_context,
  34. enhancement_options: { include_recommendations: true, include_personalization: true }
  35. ).call
  36. enhanced = response_data.dup
  37. enhanced[:user_experience] = {
  38. recommendations: enhancer.recommendations,
  39. personalization: enhancer.personalization_data,
  40. quick_actions: enhancer.generate_quick_actions,
  41. tips: enhancer.generate_contextual_tips
  42. }
  43. enhanced
  44. end
  45. private
  46. def enhance_user_experience
  47. @enhanced_response = {
  48. user_preferences: get_user_preferences,
  49. interface_settings: get_interface_settings,
  50. accessibility_options: get_accessibility_options,
  51. contextual_help: get_contextual_help
  52. }
  53. end
  54. def get_user_preferences
  55. {
  56. theme: user&.preferences&.dig('theme') || 'light',
  57. language: user&.preferences&.dig('language') || 'zh-CN',
  58. timezone: user&.preferences&.dig('timezone') || 'Asia/Shanghai',
  59. notification_settings: get_notification_settings,
  60. privacy_settings: get_privacy_settings
  61. }
  62. end
  63. def get_interface_settings
  64. {
  65. font_size: user&.interface_settings&.dig('font_size') || 'medium',
  66. compact_mode: user&.interface_settings&.dig('compact_mode') || false,
  67. animations_enabled: user&.interface_settings&.dig('animations_enabled') != false,
  68. auto_refresh_enabled: user&.interface_settings&.dig('auto_refresh_enabled') != false,
  69. refresh_interval: user&.interface_settings&.dig('refresh_interval') || 30
  70. }
  71. end
  72. def get_accessibility_options
  73. {
  74. high_contrast: user&.accessibility_settings&.dig('high_contrast') || false,
  75. large_text: user&.accessibility_settings&.dig('large_text') || false,
  76. screen_reader_support: user&.accessibility_settings&.dig('screen_reader_support') || false,
  77. keyboard_navigation: user&.accessibility_settings&.dig('keyboard_navigation') || false,
  78. reduced_motion: user&.accessibility_settings&.dig('reduced_motion') || false
  79. }
  80. end
  81. def get_notification_settings
  82. {
  83. email_notifications: user&.notification_settings&.dig('email') != false,
  84. push_notifications: user&.notification_settings&.dig('push') != false,
  85. sms_notifications: user&.notification_settings&.dig('sms') || false,
  86. notification_frequency: user&.notification_settings&.dig('frequency') || 'daily',
  87. quiet_hours: user&.notification_settings&.dig('quiet_hours') || {}
  88. }
  89. end
  90. def get_privacy_settings
  91. {
  92. profile_visibility: user&.privacy_settings&.dig('profile_visibility') || 'public',
  93. activity_visibility: user&.privacy_settings&.dig('activity_visibility') || 'friends',
  94. show_online_status: user&.privacy_settings&.dig('show_online_status') != false,
  95. allow_recommendations: user&.privacy_settings&.dig('allow_recommendations') != false,
  96. data_sharing_consent: user&.privacy_settings&.dig('data_sharing_consent') || false
  97. }
  98. end
  99. def get_contextual_help
  100. case request_context[:action]
  101. when 'create_post'
  102. {
  103. title: '创建新帖子',
  104. content: '分享您的想法、问题或经验。支持图片上传和富文本格式。',
  105. tips: [
  106. '使用清晰的标题吸引读者注意',
  107. '添加相关标签帮助他人发现您的内容',
  108. '检查拼写和语法错误'
  109. ],
  110. help_url: '/help/creating-posts'
  111. }
  112. when 'join_event'
  113. {
  114. title: '参加活动',
  115. content: '加入读书活动,与其他书友一起学习和成长。',
  116. tips: [
  117. '查看活动时间安排确保您能参与',
  118. '阅读活动要求做好准备工作',
  119. '积极参与讨论分享您的见解'
  120. ],
  121. help_url: '/help/joining-events'
  122. }
  123. else
  124. {
  125. title: '使用帮助',
  126. content: '如有疑问,请查看帮助文档或联系技术支持。',
  127. tips: [
  128. '使用搜索功能快速找到感兴趣的内容',
  129. '关注其他用户获取更新通知',
  130. '完善个人资料让其他用户更好地了解您'
  131. ],
  132. help_url: '/help'
  133. }
  134. end
  135. end
  136. def generate_recommendations
  137. recommendations = []
  138. # 基于用户行为推荐
  139. recommendations << generate_activity_recommendations
  140. recommendations << generate_content_recommendations
  141. recommendations << generate_connection_recommendations
  142. recommendations << generate_feature_recommendations
  143. recommendations.flatten.select(&:itself).first(5)
  144. end
  145. def generate_activity_recommendations
  146. # 基于用户参与的读书活动类型推荐相似活动
  147. user_activities = user&.reading_events&.where('enrollments.created_at > ?', 30.days.ago)
  148. return [] unless user_activities&.any?
  149. similar_activities = ReadingEvent.where
  150. .not(id: user_activities.pluck(:id))
  151. .where(status: :active)
  152. .where(category: user_activities.pluck(:category).uniq)
  153. .limit(3)
  154. similar_activities.map do |activity|
  155. {
  156. type: 'activity',
  157. title: "推荐活动: #{activity.title}",
  158. description: "基于您参与过的#{activity.category}类活动推荐",
  159. action_url: "/events/#{activity.id}",
  160. priority: 'high'
  161. }
  162. end
  163. end
  164. def generate_content_recommendations
  165. # 基于用户点赞和评论推荐帖子
  166. liked_categories = user&.likes&.joins(:post)
  167. .where('likes.created_at > ?', 30.days.ago)
  168. .group('posts.category')
  169. .count
  170. .keys
  171. return [] unless liked_categories&.any?
  172. popular_posts = Post.where
  173. .category: liked_categories
  174. .where('posts.created_at > ?', 7.days.ago)
  175. .order(likes_count: :desc)
  176. .limit(3)
  177. popular_posts.map do |post|
  178. {
  179. type: 'content',
  180. title: "热门帖子: #{post.title}",
  181. description: "您感兴趣的#{post.category}类别中的热门内容",
  182. action_url: "/posts/#{post.id}",
  183. priority: 'medium'
  184. }
  185. end
  186. end
  187. def generate_connection_recommendations
  188. # 推荐可能认识的用户
  189. mutual_connections = find_mutual_connections
  190. mutual_connections.first(3).map do |potential_user|
  191. {
  192. type: 'connection',
  193. title: "可能认识的用户: #{potential_user.nickname}",
  194. description: "您有#{mutual_connections[potential_user]}个共同好友",
  195. action_url: "/users/#{potential_user.id}",
  196. priority: 'low'
  197. }
  198. end
  199. end
  200. def generate_feature_recommendations
  201. features = []
  202. # 新功能推荐
  203. unless user&.preferences&.dig('new_features_shown')&.include?('reading_goals')
  204. features << {
  205. type: 'feature',
  206. title: '设置阅读目标',
  207. description: '为自己设定每月阅读目标,跟踪阅读进度',
  208. action_url: '/profile/reading-goals',
  209. priority: 'high',
  210. badge: 'NEW'
  211. }
  212. end
  213. # 功能使用提示
  214. if user&.posts&.count == 0
  215. features << {
  216. type: 'feature',
  217. title: '发布第一条帖子',
  218. description: '开始分享您的想法,与其他书友交流',
  219. action_url: '/posts/new',
  220. priority: 'medium'
  221. }
  222. end
  223. features
  224. end
  225. def find_mutual_connections
  226. # 简化版的共同好友推荐逻辑
  227. # 实际实现可以基于更复杂的社交网络分析
  228. User.joins(:received_flowers)
  229. .where(flowers: { giver_id: user.friends.pluck(:id) })
  230. .where.not(id: user.id)
  231. .distinct
  232. .limit(10)
  233. end
  234. def generate_personalization_data
  235. {
  236. user_level: calculate_user_level,
  237. achievement_progress: get_achievement_progress,
  238. reading_stats: get_reading_statistics,
  239. engagement_metrics: get_engagement_metrics,
  240. personalized_greeting: generate_personalized_greeting
  241. }
  242. end
  243. def calculate_user_level
  244. # 基于用户活跃度计算等级
  245. score = 0
  246. # 帖子贡献
  247. score += (user&.posts&.count || 0) * 10
  248. # 评论贡献
  249. score += (user&.comments&.count || 0) * 5
  250. # 点赞互动
  251. score += (user&.likes&.count || 0) * 2
  252. # 活动参与
  253. score += (user&.event_enrollments&.count || 0) * 15
  254. # 小红花获得
  255. score += (user&.received_flowers&.count || 0) * 8
  256. case score
  257. when 0..50
  258. { level: 1, title: '新手书友', next_level_score: 51, current_score: score }
  259. when 51..200
  260. { level: 2, title: '活跃书友', next_level_score: 201, current_score: score }
  261. when 201..500
  262. { level: 3, title: '资深书友', next_level_score: 501, current_score: score }
  263. when 501..1000
  264. { level: 4, title: '领读者', next_level_score: 1001, current_score: score }
  265. else
  266. { level: 5, title: '读书达人', next_level_score: nil, current_score: score }
  267. end
  268. end
  269. def get_achievement_progress
  270. # 获取用户成就进度
  271. achievements = [
  272. {
  273. id: 'first_post',
  274. name: '初次分享',
  275. description: '发布第一条帖子',
  276. progress: (user&.posts&.count || 0) >= 1 ? 100 : 0,
  277. completed: (user&.posts&.count || 0) >= 1,
  278. icon: 'post'
  279. },
  280. {
  281. id: 'ten_posts',
  282. name: '积极分享',
  283. description: '发布10条帖子',
  284. progress: [(user&.posts&.count || 0) * 10, 100].min,
  285. completed: (user&.posts&.count || 0) >= 10,
  286. icon: 'posts'
  287. },
  288. {
  289. id: 'first_comment',
  290. name: '初次评论',
  291. description: '发表第一条评论',
  292. progress: (user&.comments&.count || 0) >= 1 ? 100 : 0,
  293. completed: (user&.comments&.count || 0) >= 1,
  294. icon: 'comment'
  295. },
  296. {
  297. id: 'first_event',
  298. name: '初次参与',
  299. description: '参加第一个读书活动',
  300. progress: (user&.event_enrollments&.count || 0) >= 1 ? 100 : 0,
  301. completed: (user&.event_enrollments&.count || 0) >= 1,
  302. icon: 'event'
  303. }
  304. ]
  305. # 添加自定义成就
  306. achievements.concat(get_custom_achievements)
  307. end
  308. def get_custom_achievements
  309. # 基于用户行为的自定义成就
  310. custom_achievements = []
  311. # 连续签到成就
  312. check_in_streak = calculate_check_in_streak
  313. if check_in_streak > 0
  314. custom_achievements << {
  315. id: 'check_in_streak',
  316. name: "连续签到#{check_in_streak}天",
  317. description: '坚持每日签到',
  318. progress: [check_in_streak * 10, 100].min,
  319. completed: check_in_streak >= 10,
  320. icon: 'calendar'
  321. }
  322. end
  323. # 社交达人成就
  324. flowers_given = user&.flowers_given&.count || 0
  325. if flowers_given > 0
  326. custom_achievements << {
  327. id: 'social_butterfly',
  328. name: "送出#{flowers_given}朵小红花",
  329. description: '积极互动,鼓励他人',
  330. progress: [flowers_given * 2, 100].min,
  331. completed: flowers_given >= 50,
  332. icon: 'flower'
  333. }
  334. end
  335. custom_achievements
  336. end
  337. def calculate_check_in_streak
  338. # 计算连续签到天数
  339. return 0 unless user
  340. # 获取最近30天的签到记录
  341. check_ins = CheckIn.where(user: user)
  342. .where('created_at > ?', 30.days.ago)
  343. .order(created_at: :desc)
  344. return 0 if check_ins.empty?
  345. streak = 1
  346. check_ins.each_cons(2) do |current, previous|
  347. break unless (current.created_at.to_date - previous.created_at.to_date) == 1
  348. streak += 1
  349. end
  350. streak
  351. end
  352. def get_reading_statistics
  353. {
  354. books_read: user&.check_ins&.distinct.count(:reading_schedule_id) || 0,
  355. pages_read: calculate_pages_read,
  356. reading_time: calculate_reading_time,
  357. favorite_genres: get_favorite_genres,
  358. monthly_progress: get_monthly_progress
  359. }
  360. end
  361. def calculate_pages_read
  362. # 基于打卡数据估算阅读页数
  363. user&.check_ins&.sum(:pages_read) || 0
  364. end
  365. def calculate_reading_time
  366. # 基于打卡数据估算阅读时间
  367. total_minutes = user&.check_ins&.sum(:reading_duration) || 0
  368. hours = total_minutes / 60
  369. minutes = total_minutes % 60
  370. { hours: hours, minutes: minutes, total_minutes: total_minutes }
  371. end
  372. def get_favorite_genres
  373. # 获取用户最喜欢的阅读类型
  374. genre_counts = CheckIn.joins(reading_schedule: :reading_event)
  375. .where(user: user)
  376. .group('reading_events.category')
  377. .count
  378. genre_counts.sort_by { |_, count| -count }.first(3).map do |genre, count|
  379. { genre: genre, count: count }
  380. end
  381. end
  382. def get_monthly_progress
  383. # 获取本月阅读进度
  384. start_of_month = Time.current.beginning_of_month
  385. {
  386. posts_this_month: user&.posts&.where('created_at > ?', start_of_month).count || 0,
  387. comments_this_month: user&.comments&.where('created_at > ?', start_of_month).count || 0,
  388. events_this_month: user&.event_enrollments&.where('created_at > ?', start_of_month).count || 0,
  389. flowers_this_month: user&.received_flowers&.where('created_at > ?', start_of_month).count || 0
  390. }
  391. end
  392. def get_engagement_metrics
  393. {
  394. login_frequency: calculate_login_frequency,
  395. interaction_rate: calculate_interaction_rate,
  396. content_quality_score: calculate_content_quality_score,
  397. community_contribution: calculate_community_contribution
  398. }
  399. end
  400. def calculate_login_frequency
  401. # 计算登录频率(简化版)
  402. recent_logins = 30 # 假设数据,实际应从日志获取
  403. (recent_logins / 30.0).round(2)
  404. end
  405. def calculate_interaction_rate
  406. # 计算互动率
  407. total_interactions = (user&.comments&.count || 0) + (user&.likes&.count || 0)
  408. total_content = user&.posts&.count || 1
  409. (total_interactions.to_f / total_content).round(2)
  410. end
  411. def calculate_content_quality_score
  412. # 计算内容质量分
  413. total_likes = user&.posts&.sum(:likes_count) || 0
  414. total_comments = user&.posts&.sum(:comments_count) || 0
  415. total_posts = user&.posts&.count || 1
  416. score = ((total_likes * 2 + total_comments) / total_posts.to_f).round(2)
  417. [score, 10.0].min
  418. end
  419. def calculate_community_contribution
  420. # 计算社区贡献度
  421. contribution_score = 0
  422. # 发帖贡献
  423. contribution_score += (user&.posts&.count || 0) * 5
  424. # 评论贡献
  425. contribution_score += (user&.comments&.count || 0) * 3
  426. # 小红花贡献
  427. contribution_score += (user&.flowers_given&.count || 0) * 2
  428. # 活动贡献
  429. contribution_score += (user&.event_enrollments&.count || 0) * 8
  430. contribution_score
  431. end
  432. def generate_personalized_greeting
  433. hour = Time.current.hour
  434. time_greeting = case hour
  435. when 5..11
  436. '早上好'
  437. when 12..17
  438. '下午好'
  439. when 18..22
  440. '晚上好'
  441. else
  442. '夜深了'
  443. end
  444. user_name = user&.nickname || '书友'
  445. activity_tip = generate_activity_tip
  446. "#{time_greeting},#{user_name}!#{activity_tip}"
  447. end
  448. def generate_activity_tip
  449. case Time.current.hour
  450. when 5..9
  451. '新的一天开始了,要不要读几页书?'
  452. when 12..13
  453. '午休时间,看看书友们的分享吧'
  454. when 18..20
  455. '晚饭后是阅读的好时光'
  456. when 21..22
  457. '睡前阅读,有助于睡眠'
  458. else
  459. '注意休息,别太晚了哦'
  460. end
  461. end
  462. def generate_quick_actions
  463. actions = []
  464. case request_context[:current_page]
  465. when 'home'
  466. actions << { name: '发布新帖', url: '/posts/new', icon: 'edit' }
  467. actions << { name: '查看活动', url: '/events', icon: 'calendar' }
  468. when 'profile'
  469. actions << { name: '编辑资料', url: '/profile/edit', icon: 'user' }
  470. actions << { name: '设置', url: '/settings', icon: 'settings' }
  471. end
  472. # 基于用户状态添加快捷操作
  473. if user&.unread_notifications&.any?
  474. actions << { name: '查看通知', url: '/notifications', icon: 'bell', badge: user.unread_notifications.count }
  475. end
  476. actions
  477. end
  478. def generate_contextual_tips
  479. tips = []
  480. # 基于时间和用户行为的提示
  481. if Time.current.hour >= 22
  482. tips << {
  483. type: 'health',
  484. message: '夜深了,注意保护眼睛,适当休息',
  485. icon: 'moon'
  486. }
  487. end
  488. # 基于用户活跃度的提示
  489. if user&.last_sign_in_at && user.last_sign_in_at < 7.days.ago
  490. tips << {
  491. type: 'engagement',
  492. message: '您已经几天没有来了,看看朋友们的新动态吧',
  493. icon: 'users'
  494. }
  495. end
  496. # 新功能提示
  497. if user&.created_at && user.created_at < 30.days.ago && user.posts.count < 3
  498. tips << {
  499. type: 'encouragement',
  500. message: '分享您的读书心得,帮助更多书友',
  501. icon: 'book'
  502. }
  503. end
  504. tips
  505. end
  506. end